Commit Graph

11 Commits

Author SHA1 Message Date
bvandeusen 47d2f61161 fix: drift audit batch 1 — six small mechanical wins
test-go / test (push) Successful in 32s
test-web / test (push) Successful in 42s
test-go / integration (push) Has been cancelled
Six findings from the 2026-06-02 multi-system drift audit (Scribe
parent task #552):

- **#553 (web)** Tailwind class fix: web admin playback-errors Delete
  confirm button was using `bg-action-danger`, an undefined token —
  swap to `bg-action-destructive` to match every other destructive
  button. Restored the Oxblood signal that distinguishes Delete from
  Cancel.

- **#555 (web)** Type the `source` field on `play_started` in the
  EventRequest discriminated union. Server's eventRequest accepts it;
  web's TS type was missing the slot, so a "drop extra properties"
  refactor could silently strip the source tag and break system-
  playlist rotation attribution.

- **#556 + #557 (server)** Coverage rollup whitelist was pinned to
  ('sidecar','embedded','mbcaa','theaudiodb'); migration 0020 added
  'deezer' and 'lastfm' as valid cover_art_source values but those
  never got wired in, so albums with art from those providers
  silently counted as MISSING in the admin Coverage dashboard. The
  rollup test was seeding only the pre-0020 sources, masking the
  gap in CI. Extend the query to include deezer + lastfm; seed the
  test with one row per valid source (regression-guards future
  additions).

- **#558 (web)** Auth gate was blocking /forgot-password and
  /reset-password/<token> — both are entered without a session by
  definition, so the email-link reset flow was bouncing signed-out
  users to /login. Add /forgot-password to the public set and a
  /reset-password/ prefix matcher. New tests assert both routes
  reach their pages without redirect.

- **#571 (server)** Library scanner was indexing only .mp3/.m4a/.flac
  /.ogg while the stream handler (media.go) had been extended to
  serve .opus, .aac, and .wav. A user with .opus files in their
  library never saw them in artist/album listings because the
  scanner skipped indexing — silent data loss. Aligned the scanner
  to match the media handler.

Scribe statuses updated to in_progress; flipping to done after the
push since these are mechanical and verified directly against the
cited file:lines.
2026-06-02 18:11:25 -04:00
bvandeusen e2432caa65 fix(library): extract recording MBID → tracks.mbid; unblock LB similarity
tracks.mbid was 100% NULL: the scanner only extracted album + artist
MBIDs, never the recording MBID. The ListenBrainz similarity worker is
gated on tracks.mbid IS NOT NULL, so track_similarity could never
populate — starving For-You/radio's strongest candidate source
(lb_similar) on every library.

- mbids.go: add extractRecordingMBID (mbz.Recording / Picard
  musicbrainz_recordingid). Separate fn so extractMBIDs' signature +
  unit tests stay untouched.
- scanner.go: persist recording MBID via UpsertTrack (heals on the
  file_path conflict, so re-scans backfill for free).
- BackfillTrackMBIDs: one-shot pass mirroring BackfillMBIDs, wired as
  scan Stage 2b (idempotent via SetTrackMbidIfNull, gated by
  BackfillCap, log-only progress).
- migration 0029: tracks_mbid_unique (0002, written when the column
  was always NULL) wrongly assumes one MBID == one track. A recording
  appears on multiple releases, so rows legitimately share a recording
  MBID. Replace with a non-unique partial index. Zero-risk: column is
  100% NULL at migration time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:49:38 -04:00
bvandeusen 0fa7dc7982 feat(server): scanner + DeleteTrackFile write library_changes
Wires sync.LogChange into the library mutation sites so /api/library/sync
reflects upserts and deletes.

Architectural pivot: LogChange's signature is now (ctx, dbq.DBTX, ...) so
it works with both *pgxpool.Pool and pgx.Tx. The scanner doesn't run
mutations in explicit transactions, so it pool-binds; delete.go matches.
Tx-bound callers (likes/playlists in subsequent commits) keep atomicity.

Also: sync.FormatUUID centralizes the pgtype.UUID → canonical string
conversion that both the scanner and the sync handler need; library_sync.go
now uses it instead of a local copy.

Best-effort logging on scanner failures (Warn, don't fail the scan): a
LogChange error after a successful upsert is rare and self-healing — the
next scan that touches the entity re-emits the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:39:58 -04:00
bvandeusen 3e9e46f190 feat(server/m7-scan-progress): wire stagePublisher into 4 scan stages
Each of the 4 stage workers (library walk, MBID backfill, cover
enrich, artist art enrich) gains a progressCb parameter that
receives a snapshot of the current running state per item processed.
The orchestrator stores the snapshot in a local var that the
publisher's marshal closure reads at flush time.

scanrun.go's 4 stage blocks are rewritten to construct one publisher
per stage, pass a Tick-firing closure to the worker as progressCb,
and call Flush() after the worker returns (routing any persist error
to captureErr — preserves today's diagnostic behavior).

The previous post-stage UpdateScanRun* calls are removed; the
publisher's Flush handles the final write. Cadence: every 500 items
OR every 1s, whichever fires first.

Outside-orchestrator callers (scanner_test.go, enricher_test.go)
pass nil for progressCb — no-op behavior preserved for tests that
don't need progress observability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 21:14:50 -04:00
bvandeusen 4481874482 fix(server,web/m7-382): handle duplicate-MBID conflicts during backfill
Forward-fix from running the prior two commits in production. Backfill
on a real library exposed two further issues:

(1) gofmt -s flagged the column-aligned stubMeta methods in
    mbids_test.go. Switched to the gofmt-canonical single-space form.

(2) Many albums failed to heal with SQLSTATE 23505 (unique constraint
    albums_mbid_unique). Cause: the operator's library has duplicate
    album rows in the DB — same MusicBrainz release, split into
    multiple rows by the pre-MBID scanner because of subtle title or
    artist disagreements between files. When backfill now correctly
    extracts the MBID, two rows want to claim the same one and the
    partial unique index rejects the second.

    The conflict is correct DB behavior — we shouldn't have two rows
    with the same MBID — but it's not a write failure to alarm the
    operator about. Detect 23505 specifically:
      - downgrade the log line from Warn to Info
      - track these in a new BackfillMBIDsResult.Duplicates counter
        (separate from Skipped, which retains its "no MBID in tag"
        meaning)
      - leave the duplicate row's mbid NULL; merging duplicates is a
        separate (future) operator workflow

    Same handling threaded through the scanner heal path so a regular
    rescan doesn't generate the noise either.

    JSON tally + admin UI gain a "Duplicates" line so the operator
    can see how many duplicate rows their library carries.

Follow-up scope (separate task): a duplicate-album merge UX that
reparents the duplicate's tracks to the canonical row and deletes
the orphaned album row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:45:15 -04:00
bvandeusen 947f944fe9 fix(server/m7-382): MBID extraction never matched dhowden/tag's Raw() keys
The hand-rolled extractor in internal/library/mbids.go looked up keys that
dhowden/tag never produces, for every supported audio format:

  Vorbis (FLAC/OGG): dhowden lowercases keys at parse time (vorbis.go:59),
    we looked up MUSICBRAINZ_ALBUMID. Always missed.
  ID3v2 (MP3): dhowden stores TXXX frames under bare TXXX/TXXX_N keys
    with *tag.Comm values; the Picard tag name is on the value's
    Description field, not part of the key. We looked up
    TXXX:MusicBrainz Album Id. Always missed.
  MP4 (M4A): dhowden stores freeform iTunes atoms under their bare
    sub-name. We looked up ----:com.apple.iTunes:MusicBrainz Album Id.
    Always missed.

Net effect: every album in the m7-380 boot backfill reported
processed=N, healed=0, skipped=N. The m7-379 enricher gate then
short-circuited NULL on every album, so MBCAA was never queried.
Symptom: "all albums skipped" during cover enrichment.

The pre-existing unit tests passed because they hand-built Raw() maps
with the intended-but-incorrect keys, never round-tripping through
dhowden's parser.

Replace the hand-rolled extractor with dhowden's own mbz sub-package,
which knows the per-format Raw() conventions and uses lowercase
canonical keys (mbz.Album, mbz.Artist). Update both call sites to pass
the full tag.Metadata instead of just Raw(). Rewrite the test with a
stubMeta implementing tag.Metadata, asserting against the actual
per-format Raw() shape, plus a regression guard that pins "uppercase
Vorbis keys must not match" so the v0 bug can't sneak back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 21:15:51 -04:00
bvandeusen 6c6c4bc1e4 feat(server/m7-379): heal existing artist+album mbid on rescan 2026-05-04 19:03:12 -04:00
bvandeusen ab8fff834a feat(server/m7-379): extract MusicBrainz IDs from audio tags during scan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:01:55 -04:00
bvandeusen 4a9193bc52 feat(library): extract track duration via ffprobe
Scrubbing/seeking in clients was a no-op because every track shipped
with duration_ms=0. Shell out to ffprobe (already in the image) per
file during scan and record the parsed duration. ffprobe failures are
warned + recorded as 0 so a single bad file doesn't sink the scan.

Tightened the incremental skip to also require duration_ms > 0 so
existing libraries get backfilled on the next rescan instead of
needing a wipe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 08:02:37 -04:00
bvandeusen dcd19c0143 fix(library): drop out-of-range years instead of failing the album
Tag-supplied years were being passed straight to Postgres' date column
without validation. Files with corrupt or 5+ digit years (seen in the
wild on a couple of dozen albums) tripped SQLSTATE 22008 and the entire
album upsert failed, dropping every track on those albums from the
library.

Validate the year is within 1..9999 before constructing the date. If
it's outside that window, log a warning naming the album and year, and
insert the album with no release_date — a soft field shouldn't take
down the whole row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:14:51 -04:00
bvandeusen fdf28efef0 M1/#293: library scanner (walk + tag-parse + upsert + mtime incremental) 2026-04-19 15:16:48 +00:00