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>
This commit is contained in:
2026-10-21 11:01:23 -04:00
co-authored by Claude Opus 5.5
parent e44025435e
commit cb1f619a21
4 changed files with 118 additions and 3 deletions
+42
View File
@@ -253,3 +253,45 @@ func TestFingerprintResult_InconclusiveIfEitherHalfIs(t *testing.T) {
}
}
}
// fpcalc 1.5.1 exits 3 with "(End of file)" on every file shorter than the
// span asked for (#6178). That print covers the whole file and is kept; a file
// whose declared length runs past the span stopped early, and is not.
func TestFpcalcReadToEnd(t *testing.T) {
eof := []byte("ERROR: Error decoding audio frame (End of file)")
out := func(sec int) []byte { return []byte(fmt.Sprintf("DURATION=%d\nFINGERPRINT=AQAB\n", sec)) }
for name, tc := range map[string]struct {
err error
out []byte
want bool
}{
"short file read to its end": {&toolExitError{name: "fpcalc", code: 3, stderr: eof}, out(37), true},
"exactly the span": {&toolExitError{name: "fpcalc", code: 3, stderr: eof}, out(120), true},
"declared longer than read": {&toolExitError{name: "fpcalc", code: 3, stderr: eof}, out(150), false},
"partial for another reason": {&toolExitError{name: "fpcalc", code: 3, stderr: []byte("ERROR: invalid data")}, out(37), false},
"a different exit": {&toolExitError{name: "fpcalc", code: 2, stderr: eof}, out(37), false},
"no duration printed": {&toolExitError{name: "fpcalc", code: 3, stderr: eof}, []byte("FINGERPRINT=AQAB\n"), false},
"not a tool exit (a stall)": {fmt.Errorf("fpcalc: %w", errFingerprintTimeout), out(37), false},
"wrapped exit is still judged": {fmt.Errorf("lookup: %w", &toolExitError{name: "fpcalc", code: 3, stderr: eof}), out(37), true},
} {
if got := fpcalcReadToEnd(tc.err, tc.out, 120); got != tc.want {
t.Errorf("%s: fpcalcReadToEnd = %v, want %v", name, got, tc.want)
}
}
}
// The output of a tool that exited non-zero reaches the caller, or
// fpcalcReadToEnd has nothing to read; the message is unchanged.
func TestRunFingerprintTool_KeepsOutputOnExit(t *testing.T) {
out, err := runFingerprintTool(context.Background(), "sh",
[]string{"-c", "echo DURATION=37; echo 'ERROR: Error decoding audio frame (End of file)' >&2; exit 3"})
if err == nil {
t.Fatal("exit 3 returned no error")
}
if want := "sh exited 3: ERROR: Error decoding audio frame (End of file)"; err.Error() != want {
t.Errorf("err = %q, want %q", err, want)
}
if d, ok := fpcalcDuration(out); !ok || d != 37 {
t.Errorf("output = %q, want the DURATION line kept", out)
}
}