feat(api): SHA-256 and Chromaprint fingerprints for the library (M5.5)
A background Fingerprinter runs after every scan and at startup, one file at a time, filling media_files.sha256 and track_fingerprints (fpcalc raw, first 120 s). Without fpcalc only SHA-256 is computed and files stay queued for Chromaprint. Changing a file's content resets its prints. Similarity() compares raw prints with a ±80-point alignment search; it is the basis for duplicate detection in the upload queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed063d3763
commit
9b50038bb4
9 files changed
+406
-6
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package library
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func randomPrint(r *rand.Rand, n int) []uint32 {
|
||||
out := make([]uint32, n)
|
||||
for i := range out {
|
||||
out[i] = r.Uint32()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestSimilarity(t *testing.T) {
|
||||
r := rand.New(rand.NewPCG(1, 2))
|
||||
a := randomPrint(r, 950) // ~2 мин
|
||||
if s := Similarity(a, a); s != 1 {
|
||||
t.Fatalf("same print: %v", s)
|
||||
}
|
||||
|
||||
// Перекодирование: другое вступление (сдвиг на 20 точек) и ~8% перевёрнутых бит.
|
||||
b := append(randomPrint(r, 20), a...)
|
||||
for i := range b {
|
||||
for bit := range 32 {
|
||||
if r.IntN(100) < 8 {
|
||||
b[i] ^= 1 << bit
|
||||
}
|
||||
}
|
||||
}
|
||||
if s := Similarity(a, b); s < SameRecording {
|
||||
t.Fatalf("re-encoded copy: %v < %v", s, SameRecording)
|
||||
}
|
||||
|
||||
if s := Similarity(a, randomPrint(r, 950)); s >= SameRecording {
|
||||
t.Fatalf("different recordings: %v >= %v", s, SameRecording)
|
||||
}
|
||||
if s := Similarity(a, a[:40]); s != 0 {
|
||||
t.Fatalf("too short overlap must not match: %v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFPCalc(t *testing.T) {
|
||||
fp, err := parseFPCalc([]byte(`{"duration": 215.4, "fingerprint": [4294967295, 1, 2]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fp.DurationMs != 215400 || len(fp.Points) != 3 || fp.Points[0] != 4294967295 {
|
||||
t.Fatalf("parsed %+v", fp)
|
||||
}
|
||||
if fp.Prefix() != int64(-4294967295) { // 0xFFFFFFFF_00000001 как int64
|
||||
t.Fatalf("prefix %d", fp.Prefix())
|
||||
}
|
||||
if _, err := parseFPCalc([]byte(`{"duration": 1, "fingerprint": []}`)); err == nil {
|
||||
t.Fatal("empty fingerprint must fail")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user