Files
minstrel/internal/db/migrations/0069_acoustid_lookup.up.sql
T
bvandeusenandClaude Opus 5.5 3c575b137c
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
feat(library): AcoustID lookup worker fills the MBIDs tags leave empty (M401 #3920 #3921)
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>
2026-10-06 23:26:03 -04:00

70 lines
3.6 KiB
SQL

-- 0069_acoustid_lookup.up.sql — recording MBIDs looked up through AcoustID
-- for tracks whose tags carry none (Scribe milestone #401).
--
-- A track with no recording MBID is invisible to the ListenBrainz similarity
-- arm in both directions, so this fills the ids the tags leave empty.
-- Where tracks.mbid came from. The file's own tag always outranks a lookup
-- (M401 D4): the scan's upsert keeps a looked-up id while the tag carries
-- none, and replaces it the moment the tag carries one.
ALTER TABLE tracks ADD COLUMN mbid_source text;
-- Every id stored so far was read from a tag (the scan or the tag backfill),
-- or inherited from a merged duplicate's tag.
UPDATE tracks SET mbid_source = 'tag' WHERE mbid IS NOT NULL;
ALTER TABLE tracks
ADD CONSTRAINT tracks_mbid_source_check CHECK (mbid_source IN ('tag', 'acoustid')),
ADD CONSTRAINT tracks_mbid_source_pair CHECK ((mbid IS NULL) = (mbid_source IS NULL));
-- What happened the last time a track was looked up. A table of its own, as
-- track_loudness is (0065): tracks is read with SELECT * on the hot path.
--
-- What a row means, which the lookup worker depends on:
-- no row never looked up, or the file changed since (the scan deletes
-- the row): look it up
-- matched one recording cleared the score threshold, or one was left
-- after disambiguating by title and length; recording_mbid is it
-- ambiguous several recordings cleared it and nothing told them apart.
-- Nothing is written: a wrong MBID would feed similarity the
-- wrong neighbours, which is worse than none (M401 D5)
-- no_match AcoustID linked nothing at or above the threshold
-- failed fpcalc rejected the file, or AcoustID rejected its print
-- Every state is settled until the file changes. A failure that says nothing
-- about the file (AcoustID unreachable, a timeout, a cancelled pass) writes no
-- row, so the worker tries again.
CREATE TABLE track_acoustid_lookups (
track_id uuid PRIMARY KEY REFERENCES tracks (id) ON DELETE CASCADE,
state text NOT NULL,
-- The best score any linked recording had; NULL when none was linked.
best_score real,
-- How many distinct recordings were linked, at any score.
candidates integer NOT NULL DEFAULT 0,
recording_mbid text,
-- Why a lookup failed, for the admin list.
detail text,
looked_up_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT track_acoustid_lookups_state_check
CHECK (state IN ('matched', 'ambiguous', 'no_match', 'failed')),
CONSTRAINT track_acoustid_lookups_matched_has_mbid
CHECK ((state = 'matched') = (recording_mbid IS NOT NULL))
);
CREATE INDEX track_acoustid_lookups_state ON track_acoustid_lookups (state);
-- AcoustID's knobs, in admin Settings (rule 25). Singleton in the style of
-- loudness_settings (0065).
CREATE TABLE acoustid_settings (
id boolean PRIMARY KEY DEFAULT true,
-- Off by default (rule 26): AcoustID needs a key the operator registers,
-- and the lookup sends fingerprints to a third party.
enabled boolean NOT NULL DEFAULT false,
api_key text,
-- The score a recording needs before it is considered at all. AcoustID
-- scores a true match close to 1; 0.85 leaves room for a lossy encode.
min_score real NOT NULL DEFAULT 0.85,
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT acoustid_settings_singleton CHECK (id = true),
CONSTRAINT acoustid_settings_min_score_range CHECK (min_score >= 0.5 AND min_score <= 1)
);
INSERT INTO acoustid_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;