M401: AcoustID recording-id lookup #137

Merged
bvandeusen merged 5 commits from dev into main 2026-10-07 09:50:12 -04:00
Owner

Fills the MusicBrainz recording ids that file tags leave empty, so the similarity arms can place untagged tracks.

  • Client (#3919): compressed fpcalc print (first 120s), gzip POST to AcoustID's v2 lookup, 20s deadline, spaced to stay under 3 requests per second.
  • Provenance (#3920): migration 0069 adds tracks.mbid_source (tag | acoustid), the track_acoustid_lookups state table and the acoustid_settings singleton. A tag id always wins; a rescan never drops a looked-up id.
  • Worker (#3921): writes an id only for an unambiguous match (one candidate at or above min score, or exactly one left after matching title and length within 3s). Stops a pass on a refused key or an unreachable service and reports why.
  • Admin UI (#3922): Admin → Integrations → AcoustID card with the switch, a write-only key, the min score, coverage by source, "Look up now" and the unsettled list.

Off by default. Does nothing until a key is entered and the switch is on.

CI: run 8494 green on every lane, including the APK and image builds, for e8eee553.

🤖 Generated with Claude Code

Fills the MusicBrainz recording ids that file tags leave empty, so the similarity arms can place untagged tracks. - **Client (#3919):** compressed fpcalc print (first 120s), gzip POST to AcoustID's v2 lookup, 20s deadline, spaced to stay under 3 requests per second. - **Provenance (#3920):** migration 0069 adds `tracks.mbid_source` (tag | acoustid), the `track_acoustid_lookups` state table and the `acoustid_settings` singleton. A tag id always wins; a rescan never drops a looked-up id. - **Worker (#3921):** writes an id only for an unambiguous match (one candidate at or above min score, or exactly one left after matching title and length within 3s). Stops a pass on a refused key or an unreachable service and reports why. - **Admin UI (#3922):** Admin → Integrations → AcoustID card with the switch, a write-only key, the min score, coverage by source, "Look up now" and the unsettled list. Off by default. Does nothing until a key is entered and the switch is on. CI: run 8494 green on every lane, including the APK and image builds, for e8eee553. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 5 commits 2026-10-07 09:50:06 -04:00
internal/acoustid posts AcoustID's v2 lookup as a gzip form with
meta=recordings, one request per 400ms (their limit is 3/s) and a 20s
deadline. It returns every linked recording with its best score; choosing
among them is the worker's job (D5). The server's error codes map to an
invalid key (stop and say so), a rejected fingerprint (a verdict on the
track) or unavailable (try again later). A cancelled caller stays a
cancellation.

fpcalcLookupArgs and parseFpcalcCompressed read fpcalc's default output,
the compressed string the lookup takes (D1), always over the first 120s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(library): AcoustID lookup worker fills the MBIDs tags leave empty (M401 #3920 #3921)
release / web (push) Successful in 1m44s
release / go (push) Successful in 2m1s
release / govulncheck (push) Successful in 17s
release / integration (push) Successful in 5m22s
release / android (push) Successful in 5m48s
release / Build signed APK (releases and dev) (push) Successful in 5m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m6s
release / Verify release artifacts (tag releases only) (push) Skipped
3c575b137c
Migration 0069 adds tracks.mbid_source (tag | acoustid), a lookup state per
track (matched | ambiguous | no_match | failed) and the acoustid_settings
row (off, no key, min score 0.85).

The file's tag outranks a lookup (D4). UpsertTrack keeps a looked-up id
through a re-read that finds no tag id and replaces it as soon as one
appears. SetTrackMbidFromAcoustID refuses to write over a tag id.

The worker fingerprints each untagged track with fpcalc's compressed
print, looks it up and writes an id only when D5 settles it: one
recording at or above the threshold, or one left after matching title and
length. Ambiguous and no-match results write nothing. A key AcoustID
refuses, or the service being unreachable, stops the pass and is reported
in the worker's status. It never counts as a verdict on a track.

A changed file drops its lookup in the scan. The re-lookup takes back an
id that no longer matches.

Admin API: GET /api/admin/library/acoustid (settings, status, coverage by
source), PUT …/acoustid-settings (write-only key), POST …/acoustid/run,
GET …/acoustid/unsettled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(web): AcoustID card on Integrations — key, threshold, coverage by source (M401 #3922)
release / go (push) Successful in 2m25s
release / web (push) Failing after 26s
release / govulncheck (push) Successful in 21s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m34s
release / android (push) Canceled after 1m3s
release / Build signed APK (releases and dev) (push) Canceled after 1m3s
0a7f788390
The card takes the slot of the unimplemented "MusicBrainz overrides"
placeholder. Rows:
- the on switch
- a write-only key field (the stored key is never sent back), with a
  link to register an application
- the minimum score (0.5 to 1)

Below them, recording-id coverage reads as a column: from tags, looked
up, none, and of the none how many are waiting, no match, ambiguous or
failed. There is a "Look up now" button and a folded list of the tracks
the lookup could not settle.

Off, keyless and stopped-short passes are each a visible state with the
reason (rule 164).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(web): send the empty body api.post requires for AcoustID run-now (M401 #3922)
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 1m18s
release / go (push) Successful in 1m43s
release / Build + push container image (push) Canceled after 0s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 4m51s
release / android (push) Canceled after 4m51s
release / Build signed APK (releases and dev) (push) Canceled after 4m6s
03e07e7c66
svelte-check on 0a7f7883: api.post takes a body. Every other body-less POST
in the client passes {}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(web): AcoustID card's loading line names itself (M401 #3922)
release / integration (push) Successful in 6m4s
release / android (push) Successful in 8m1s
release / Build signed APK (releases and dev) (push) Successful in 8m22s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m31s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 28s
release / go (push) Successful in 1m41s
release / web (push) Successful in 1m21s
e8eee55325
Its bare "Loading…" made the Integrations page's cover-providers test find
two matches for /loading…/i (Vitest, run 8487). "Loading AcoustID
settings…" also says which card is loading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bvandeusen merged commit 09bd71629a into main 2026-10-07 09:50:12 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#137