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>
This commit is contained in:
2026-10-06 23:16:52 -04:00
co-authored by Claude Opus 5.5
parent b28cbe0600
commit f13da62797
4 changed files with 621 additions and 0 deletions
+61
View File
@@ -60,6 +60,67 @@ func TestParseFpcalcRaw(t *testing.T) {
}
}
// The lookup print is fpcalc's default output, kept as the string it printed.
func TestParseFpcalcCompressed(t *testing.T) {
cases := []struct {
name string
out string
want lookupFingerprint
wantErr string
}{
{
name: "default output",
out: "DURATION=641\nFINGERPRINT=AQABz0qUkZK4oOfhL-CPc4e5C_wW2H2QH9uDL4cvoT8UNQ-eHtsE8cceeFJx\n",
want: lookupFingerprint{fingerprint: "AQABz0qUkZK4oOfhL-CPc4e5C_wW2H2QH9uDL4cvoT8UNQ-eHtsE8cceeFJx", durationSec: 641},
},
{
name: "line order does not matter",
out: "FINGERPRINT=AQAB\nDURATION=3\n",
want: lookupFingerprint{fingerprint: "AQAB", durationSec: 3},
},
// A -raw invocation by mistake: a list of numbers is not a compressed
// print, and sent to AcoustID it would only come back as "no match".
{name: "raw list is refused", out: "DURATION=213\nFINGERPRINT=-1453821711,17\n", wantErr: "not a compressed print"},
{name: "standard base64 is refused", out: "DURATION=213\nFINGERPRINT=AQAB+/==\n", wantErr: "not a compressed print"},
{name: "empty fingerprint", out: "DURATION=213\nFINGERPRINT=\n", wantErr: "no fingerprint"},
{name: "no duration", out: "FINGERPRINT=AQAB\n", wantErr: "no DURATION= line"},
{name: "zero duration", out: "DURATION=0\nFINGERPRINT=AQAB\n", wantErr: "too short"},
{name: "fractional duration", out: "DURATION=12.5\nFINGERPRINT=AQAB\n", wantErr: "duration"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseFpcalcCompressed([]byte(tc.out))
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("err = %v, want one containing %q", err, tc.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if got != tc.want {
t.Fatalf("got %+v, want %+v", got, tc.want)
}
})
}
}
func TestFpcalcLookupArgs_RequestCompressedOutput(t *testing.T) {
args := fpcalcLookupArgs("/music/a.flac")
for _, flag := range []string{"-raw", "-signed"} {
if slices.Contains(args, flag) {
t.Errorf("fpcalcLookupArgs has %s, so fpcalc would not print the compressed form: %v", flag, args)
}
}
if !followedBy(args, "-length", "120") {
t.Errorf("fpcalcLookupArgs does not fingerprint fpcalc's default 120s: %v", args)
}
if args[len(args)-1] != "/music/a.flac" {
t.Errorf("path is not last: %v", args)
}
}
func TestParseStreamHash(t *testing.T) {
// The real value ffmpeg printed for both files of the #3885 pair.
const www = "24e2daa3b4a534ff1a8d1a76f67810205869daf89f728d83a16625da4d28a18e"