Commit Graph

96 Commits

Author SHA1 Message Date
bvandeusen 9bf4b504b3 feat(server): GET /api/library/sync endpoint
Returns batched upserts + deletes since the supplied cursor. Empty cursor
returns full snapshot; subsequent calls pull deltas. Per-user entities
(likes, playlists) are scoped to the authed user. Composite-key entities
(likes, playlist_tracks) use stable string ids encoded by sync.EncodeLikeID
/ sync.EncodePlaylistTrackID.

Behavior:
  204 No Content - no changes since cursor
  200 OK         - JSON syncResponse {cursor, upserts, deletes}
  410 Gone       - cursor older than oldest log row; client must reset
  401 / 500      - standard envelope errors

Adds sqlc queries GetAlbumsByIDs, GetTracksByIDs, GetPlaylistsByIDs to
mirror the existing GetArtistsByIDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:36:26 -04:00
bvandeusen cb35133843 feat(server): library_changes log table + sqlc queries
Append-only change log for library entities. Every mutation on
artists/albums/tracks/likes/playlists/playlist_tracks will write a row
in the same transaction as the mutation itself (wired in subsequent
commits). Powers the Flutter delta-sync endpoint (#357).

- 0025_library_changes migration (up + down)
- internal/db/queries/library_changes.sql (Insert, GetSince, MaxCursor, MinCursor)
- regenerated dbq from sqlc

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:28:26 -04:00
bvandeusen bffa397250 refactor(server/sql): unify *ForUser track queries via nullable user_id (A1) 2026-05-08 08:15:19 -04:00
bvandeusen a509ec538b feat(db/m7-user-mgmt): migration 0024 + self-service + SMTP queries (U3)
Adds users.email (optional, lowercase-unique via partial index),
smtp_config singleton, and password_resets tokens. Plus the
queries U3-T2 / T3 / T4 use:

- ChangeUserPassword: self-service password change. HTTP layer
  verifies the current password before this fires.
- UpdateUserProfile: set display_name + email together.
- RegenerateApiToken: invalidate old API token.
- GetUserByEmail: case-insensitive lookup for forgot-password.
- GetSMTPConfig + UpdateSMTPConfig: admin SMTP settings CRUD.
- CreatePasswordReset / GetPasswordReset / UsePasswordReset
  (:execrows for atomic claim) / DeleteExpiredPasswordResets
  (cron-style cleanup, not yet wired).

Email column is nullable; users without email have admin-reset
as their only password-recovery path. The lower() unique index
allows many NULLs and prevents case-insensitive duplicates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:34:44 -04:00
bvandeusen 46d38afe2d feat(server/m7-user-mgmt): admin user CRUD endpoints (U2)
Four new admin endpoints under existing RequireAdmin middleware:

- POST /api/admin/users — admin-creates-user. Body
  {username, password, display_name?, is_admin?}. Same username
  + password validation as the public /register handler. 409
  on duplicate. Audits ActionCreateUserAdmin.

- DELETE /api/admin/users/{id} — hard delete. Last-admin guard
  refuses delete when target is the only admin (409). Schema's
  ON DELETE CASCADE on user-FK tables handles plays/likes/
  sessions cleanup. Audits ActionDeleteUser with target's
  username + was_admin flag.

- POST /api/admin/users/{id}/reset-password — body
  {password}. 8-char minimum. Admin sets a new password
  without knowing the old one. Audits ActionPasswordResetAdmin.

- PUT /api/admin/users/{id}/auto-approve — body
  {auto_approve: bool}. Toggles the per-user flag added in T1
  (the #355 sub-feature surface). Audits ActionAutoApproveToggle.

adminUserView shape extended with auto_approve_requests so the
list and toggle responses carry the flag. ListUsers SQL query
updated to include auto_approve_requests; generated Go updated
to match. Tests cover happy paths, last-admin guard on delete,
password validation, duplicate username, and the auto-approve
round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:17:14 -04:00
bvandeusen 6b07eaa44d feat(db/m7-user-mgmt): migration 0023 + admin-user CRUD queries
Adds users.auto_approve_requests boolean default false (the #355
sub-feature surface; the request-flow handler that honors this
flag is U2.5 follow-up work).

Four new sqlc queries for the U2 admin endpoints:
- CreateUserAdmin: admin-driven user creation, accepts all five
  fields explicitly.
- DeleteUser: hard delete; schema's ON DELETE CASCADE foreign
  keys handle plays/likes/sessions cleanup. Last-admin guard
  lives in the HTTP layer (next task).
- ResetUserPassword: admin sets a new hashed password without
  knowing the old one.
- UpdateUserAutoApprove: toggles the new boolean.

Self-service equivalents (knows-current-password change, etc.)
are U3 work and use distinct queries.
2026-05-07 12:12:18 -04:00
bvandeusen bd362ab384 feat(server/m7-user-mgmt): admin endpoints (invites, users, promote/demote)
Five admin endpoints under existing RequireAdmin middleware:

- GET /api/admin/invites — list active + recently-redeemed
- POST /api/admin/invites — generate 24h invite, returns
  {token, expires_at, ...}. Audits ActionInviteCreate.
- DELETE /api/admin/invites/{token} — revoke unredeemed invite.
  Audits ActionInviteRevoke.
- GET /api/admin/users — list all users (id, username,
  display_name, is_admin, created_at).
- PUT /api/admin/users/{id}/admin — toggle is_admin with
  last-admin guard. Audits ActionPromoteAdmin / ActionDemoteAdmin.

Last-admin guard counts admins, refuses demotion of the sole
admin with 409 'last_admin'. Race window between count and update
is acceptable for v1 — worst case is 'no admins left,' which the
env-driven bootstrap or CLI reset can recover from. Common path
('admin demotes themselves') is now blocked.

Adds ListUsers, CountAdmins, UpdateUserAdmin sqlc queries to
users.sql.

Tests cover: invite create/list/delete round-trip, non-admin gets
403, user list, promote happy path, last-admin demotion refused,
two-admins demotion allowed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:01:16 -04:00
bvandeusen 4845628ae4 feat(db/m7-user-mgmt): migration 0022 + audit helper
Schema for user management U1: display_name on users; user_invites;
registration_settings singleton (default 'invite_only'); audit_log.
Plus the internal/audit package centralizing the action-name
vocabulary and JSON metadata marshaling so handlers don't repeat
boilerplate.

Race-safe first-admin uses a query-shape primitive
(CreateUserFirstAdminRace's WHERE NOT EXISTS subquery) rather than
a schema-level constraint. Concurrent empty-state registrations
both see 'no users yet' and both insert as admin — fine, having
two admins from the start is benign; what matters is at-least-one.
users.username uniqueness arbitrates if the two callers picked the
same username.

CreateUser signature gains display_name (nullable); existing
bootstrap call sites pass nil. The audit package declares the full
U1+U2+U3 action vocabulary upfront so subsequent slices are purely
additive on the caller side.

Tests cover audit Write with + without metadata and that every
declared action constant persists correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 11:51:00 -04:00
bvandeusen 0ba31c7816 feat(server/playlists): manual Discover refresh endpoint
POST /api/playlists/system/discover/refresh re-runs the system
playlist build synchronously for the calling user, then returns
their newly-built Discover playlist's UUID and track count.

Used by the frontend's "Refresh" affordances (next task) on the
Discover detail page header and the home Playlists row tile kebab.
Operator's escape hatch when they want a different randomness
without waiting for tomorrow's cron tick (e.g., after pasting a
Last.fm key, after the daily cover-art enrichment expanded their
playable library, or just for the heck of it).

Authenticated user only; the authed sub-router's RequireUser
middleware handles 401. Each user refreshes only their own
Discover — no admin route, no cross-user impersonation.

Adds GetSystemPlaylistByVariantForUser sqlc query for the response
lookup. Returns playlist_id=null and track_count=0 if the build
succeeded but the user's library yielded no eligible tracks
(degenerate empty-library).
2026-05-07 08:39:26 -04:00
bvandeusen a98a106f6f feat(db/m7-discover): migration 0021 + bucket queries
Adds 'discover' as an accepted system_variant on the playlists
table — alongside 'for_you' and 'songs_like_artist' — and three
sqlc bucket queries that back the new playlist's daily candidate
selection.

The three buckets surface tracks the user hasn't played and
hasn't liked:

- Dormant artists: artists this user has played < 10 times total.
  Surfaces the long tail of their library.
- Cross-user likes: tracks any OTHER user has liked but this one
  hasn't engaged with. Empty on single-user servers; the Go-side
  allocator redistributes the deficit across the other two
  buckets.
- Random unheard: pure random sample as the safety net and the
  cold-start fallback.

All three share exclusion filters: not-played by this user, not
liked by this user, not in lidarr_quarantine for this user. All
order by md5(track_id || dateStr) for daily determinism — same
pattern as the existing tieBreakHash logic in system.go.

LIMIT values are generous (80/60/200) so the per-album (<=2) /
per-artist (<=3) caps and slot redistribution in T2 have headroom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:31:17 -04:00
bvandeusen 425f12db38 refactor(server/coverart): provider interface takes ArtistRef/AlbumRef
Changes the AlbumCoverProvider and ArtistArtProvider interfaces from
MBID-only string parameters to ref-struct signatures so name-based
providers (Deezer, Last.fm in upcoming tasks) can act on rows
without MBIDs.

Today, 53 of 213 artists in the dev DB have NULL MBIDs — featured
artists, remixers, and compilation contributors created from
track-tag splits where the primary-artist MBID is in the file but
collaborator IDs aren't. These rows are unreachable by the current
MBID-only chain. Refactoring the interface unblocks future
name-based providers from acting on them.

TheAudioDB and MBCAA keep MBID-only behavior internally: they return
ErrNotFound when ref.MBID is empty rather than attempting a
name-based fallback. The MBID guard inside EnrichAlbum/EnrichArtist
is removed; providers receive the ref unconditionally and decide
whether they can act on it.

GetAlbumWithFirstTrackPath now JOINs artists to return artist_name
alongside the existing fields, supporting AlbumRef construction.

No behavioral change today (only TheAudioDB + MBCAA registered, both
keep MBID-only behavior). The change unblocks T3 (Deezer) and T4
(Last.fm) which can now register and act on every artist/album
regardless of MBID presence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 07:44:34 -04:00
bvandeusen 9a705d0ba4 feat(db/m7-art-providers): migration 0020 + provider-hash queries
Relaxes the artists/albums art_source CHECK constraints to allow the
new 'deezer' and 'lastfm' source values alongside the existing
accepted set. Adds cover_art_sources_meta.last_registered_providers_hash
for the boot-time auto-bump detection: when the registered-provider
set changes between deploys, we bump current_version once so 'none'
rows become eligible for retry against the new chain.

Two sqlc queries added: GetCoverArtProvidersHash reads the stored
hash; BumpVersionAndSetProvidersHash atomically increments the
version and stores a new hash, returning the new version. Both used
by the boot logic in a later task.
2026-05-07 07:39:02 -04:00
bvandeusen e27d030e68 feat(db/m7-recurring-scans): migration 0019 + sqlc queries
Adds the singleton scan_schedule table holding the operator's
recurring-scan config. mode is the discriminator (off/interval/
daily/weekly); CHECK constraints enforce per-mode field validity
(interval_hours required for interval; time_of_day for daily and
weekly; weekly_day 1..7 for weekly). Seed row is mode='off'.

Two new queries: GetScanSchedule (read singleton; used by scheduler
boot + admin GET) and UpdateScanSchedule (admin PATCH; application
normalizes per-mode fields to NULL when mode='off').
2026-05-06 22:12:27 -04:00
bvandeusen 5c386dc73e fix(server,web/m7-cover-sources): forward-fix CI
Three failures from the prior push:

1. internal/api/library.go and 3 other callers of q.GetArtistByID
   broke because T2 modified the query to use an explicit column
   list, making sqlc generate GetArtistByIDRow instead of returning
   dbq.Artist. dbq.Artist already has every column added by
   migration 0018 (artist_thumb_path / artist_fanart_path /
   artist_art_source / artist_art_sources_version), so the explicit
   column list was unnecessary work. Revert the SELECT to SELECT *,
   regenerate sqlc, GetArtistByIDRow disappears, all callers compile
   again.

2. internal/tracks/service_test.go missed the dataDir parameter
   added to NewService in T9. Add "" as the 4th arg to every test
   call site (tests don't exercise the artist-art cleanup path; the
   empty dataDir is fine).

3. integrations.test.ts:356's mock.calls.filter callback had a
   too-narrow type annotation that svelte-check rejected. Relax to
   any[] to match what mock.calls actually is.
2026-05-06 13:22:36 -04:00
bvandeusen 00cd28e79b feat(server,web/m7-cover-sources): scan-orchestrator 4th stage
Adds an artist-art-enrich stage after cover-enrich in RunScan. The
stage drains EnrichArtistBatch with a configurable cap and the
operator's data_dir, persists processed/succeeded/failed tallies to
the scan_runs.artist_art_enrich jsonb column added in migration
0018.

The admin /admin Library Scan card grid expands from 3 to 4 columns
with the new "Artist art" card mirroring the cover-enrichment one
(Processed / Succeeded / Failed). The TS ScanStatus type gains an
optional artist_art_enrich field. The Go scanStatusResp gains the
matching ArtistArtEnrich json.RawMessage field.

cmd/minstrel/main.go (and admin scan-trigger handler) wire the new
RunScanConfig fields: ArtistEnrichCap reuses cfg.Library.CoverArtBackfillCap
for now (can be split if separate budgets are useful), DataDir is
cfg.Storage.DataDir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:03:36 -04:00
bvandeusen 526a78b302 feat(db/m7-cover-sources): SQL queries for provider settings + recheck
- Extends GetAlbumCoverageRollup's with_art FILTER to include
  'theaudiodb' so F's coverage gauge surfaces TheAudioDB-found rows
  as with_art rather than silently undercounting.
- Modifies ListAlbumsMissingCover to take the current sources_version
  as a parameter; eligibility now includes stale-'none' rows.
- Adds SetAlbumCoverWithVersion (atomic write of path + source +
  version stamp) and the parallel SetArtistArtWithVersion +
  ClearArtistArtNone + ListArtistsMissingArt for the artist-art
  enricher.
- New coverart_settings.sql with ListProviderSettings,
  UpsertProviderSettings (boot reconciliation, preserves operator
  values on conflict), UpdateProviderSettings (admin PATCH; tri-
  state api_key handling), GetCurrentSourcesVersion,
  BumpSourcesVersion (RETURNING the new value).
2026-05-06 12:28:18 -04:00
bvandeusen a86897b35e feat(db/m7-cover-sources): migration 0018 — artist art + provider settings
Adds the schema for the pluggable cover-art provider abstraction:

- Extends albums.cover_art_source CHECK to accept 'theaudiodb' (the
  constraint from migration 0016 only allowed embedded/sidecar/mbcaa/
  none, which would block writes from the new provider).
- Adds artist_thumb_path / artist_fanart_path / artist_art_source
  on artists, with a parallel CHECK constraint and source index.
- Adds *_sources_version stamps on both albums and artists for the
  per-row recheck eligibility logic. Default 0 so every existing row
  becomes stale relative to the seeded current_version=1, retrying
  through the new chain on first scan after migrate.
- New cover_art_provider_settings table (per-provider enabled / api_key
  / display_order) and cover_art_sources_meta singleton holding
  current_version. Seeds both v1 providers (mbcaa + theaudiodb) as
  enabled.
- New artist_art_enrich jsonb column on scan_runs for the 4th
  scan-orchestrator stage tally.

The seed leaves theaudiodb.api_key NULL; the application supplies the
upstream's documented test key (2) as the default when the column is
NULL, per the no-coercive-settings principle.
2026-05-06 12:24:36 -04:00
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