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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user