3 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 cb1f619a21 fix: a track shorter than the fingerprint span keeps its print (#6178)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m26s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 5m14s
release / android (push) Successful in 5m15s
release / Build signed APK (releases and dev) (push) Successful in 5m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m18s
release / Verify release artifacts (tag releases only) (push) Skipped
The image's fpcalc 1.5.1 exits 3 with "Error decoding audio frame (End
of file)" whenever it reaches the end of a file before -length, at any
length, so every track under two minutes failed both the AcoustID lookup
print and the duplicate-detection print: 471 lookups and all 1,979
present short tracks' chromaprints.

- runFingerprintTool returns the tool's stdout beside a toolExitError.
- fpcalcReadToEnd keeps a partial exit whose stderr is the end of file
  and whose declared DURATION is within the span asked for. A damaged
  file that stops early declares more than it read, and still fails.
- Migration 0097 clears the stored failures so the lookup worker and
  the fingerprint backfill take those tracks again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:01:23 -04:00
bvandeusenandClaude Opus 5.5 f13da62797 feat(library): AcoustID lookup client and the compressed fpcalc print (M401 #3919)
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>
2026-10-06 23:16:52 -04:00
bvandeusenandClaude Opus 5 cba77a5187 feat(library): fingerprint every new or changed file — M400 #3905-#3907
test-go / test (push) Successful in 1m9s
test-go / integration (push) Successful in 3m28s
release / Build signed APK (releases and dev) (push) Successful in 4m38s
release / Build + push container image (push) Successful in 1m26s
release / Verify release artifacts (tag releases only) (push) Skipped
Two identities per track, because they answer different questions:

- audio_stream_sha256: SHA-256 of the ENCODED audio packets
  (ffmpeg -map 0:a -c:a copy -f hash). Equal means identical audio
  whatever the tags say. Measured against the #3885 pair: the two WWW
  files hash identically here and differently as whole files. Packets
  rather than decoded samples, so an ffmpeg upgrade cannot silently
  change every stored hash, and nothing is decoded.
- chromaprint: fpcalc -raw -signed. The same recording at another
  bitrate or codec, for the acoustic tier.

fpcalc ships in the image (libchromaprint-tools); shelled out because
CGO_ENABLED=0 rules out bindings.

Stored in a track_fingerprints table rather than on tracks: eight
queries read tracks with SELECT *, including album pages, search and
the Subsonic surface, and a ~4 KB array there would be de-TOASTed on
every one of them.

The scan fingerprints only bytes it has not seen (a new path, or mtime
past the row's). A tag-repair pass leaves fingerprints alone, and
unchanged files with no fingerprint are the backfill's job (#3908).
Folding that into the skip check would re-decode the whole library on
the first scan after upgrade and push a sync change per track.

A failure that says nothing about the file (timeout, cancelled scan,
tool not installed) is never stored, and on changed bytes it removes
the old row. A tool that rejects the file stores NULL at the current
version, so the backfill does not retry it every boot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-11 13:21:15 -04:00