Some checks failed
CI / Test (push) Failing after 6m18s
CI / Build (push) Successful in 1m32s
CI / Build-1 (push) Successful in 1m55s
CI / Build-2 (push) Successful in 1m33s
CI / Build-3 (push) Successful in 1m32s
CI / Build-4 (push) Successful in 1m35s
CI / Build-5 (push) Successful in 1m33s
CI / Lint (push) Failing after 2m50s
CI / Coverage (push) Successful in 2m58s
CI / Vet (push) Successful in 2m7s
Tras cada scan, localiza la intro (OP) y los créditos (ED) comparando fingerprints chromaprint entre episodios de la misma temporada — reimplementación limpia del enfoque de Intro Skipper: índice invertido de uint32, alineamiento por shifts, Hamming ≤6/32, región contigua más larga (15-120s intro / 15-450s créditos). Películas: inicio de créditos por rachas de blackframe (solo keyframes, -skip_frame nokey) que llegan al final del fichero. - fpcalc se auto-descarga de las releases estáticas de acoustid (linux/macos/windows, ~2MB) con el mismo patrón que ffmpeg/ffprobe. - Resultados cacheados como sidecar .skipseg.json (mtime + versión de algoritmo); solo los ficheros nuevos trabajan. - Submit a /api/internal/agent/skip-segments DESPUÉS del library-sync, en dos fases (episodios primero, películas después) para que la fase rápida no espere a los blackframe lentos sobre NAS. - Agrupación por (dir + título-pre-SxxEyy + season): los títulos parseados arrastran nombre de episodio y tags de release. - Gotcha cazado en vivo: fpcalc -length sale sin drenar el pipe; hay que cerrar nuestro read-end o ffmpeg queda bloqueado para siempre. - config: library.skip_detect (default true, backfill) y scan_interval default 24h → 1h (estilo Plex).
121 lines
3.1 KiB
Go
121 lines
3.1 KiB
Go
package mediainfo
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// lcg is a tiny deterministic pseudo-random stream for synthetic fingerprints.
|
|
type lcg struct{ state uint64 }
|
|
|
|
func (l *lcg) next() uint32 {
|
|
l.state = l.state*6364136223846793005 + 1442695040888963407
|
|
return uint32(l.state >> 32)
|
|
}
|
|
|
|
func TestFindSharedRegion_DetectsAlignedSegment(t *testing.T) {
|
|
// Shared segment: 700 points ≈ 86.7s — a typical anime OP.
|
|
shared := make([]uint32, 700)
|
|
g := &lcg{state: 42}
|
|
for i := range shared {
|
|
shared[i] = g.next()
|
|
}
|
|
|
|
// a: 80 points of unique noise, then the shared segment, then noise.
|
|
ga := &lcg{state: 1001}
|
|
a := make([]uint32, 0, 2000)
|
|
for i := 0; i < 80; i++ {
|
|
a = append(a, ga.next())
|
|
}
|
|
a = append(a, shared...)
|
|
for len(a) < 2000 {
|
|
a = append(a, ga.next())
|
|
}
|
|
|
|
// b: 480 points of different noise, then the same shared segment.
|
|
gb := &lcg{state: 2002}
|
|
b := make([]uint32, 0, 2000)
|
|
for i := 0; i < 480; i++ {
|
|
b = append(b, gb.next())
|
|
}
|
|
b = append(b, shared...)
|
|
for len(b) < 2000 {
|
|
b = append(b, gb.next())
|
|
}
|
|
|
|
r := FindSharedRegion(a, b, 15, 120)
|
|
if r == nil {
|
|
t.Fatal("expected a shared region, got nil")
|
|
}
|
|
wantAStart := 80 * ChromaprintSampleDur
|
|
wantBStart := 480 * ChromaprintSampleDur
|
|
if math.Abs(r.AStart-wantAStart) > 2 {
|
|
t.Errorf("AStart = %.1f, want ≈ %.1f", r.AStart, wantAStart)
|
|
}
|
|
if math.Abs(r.BStart-wantBStart) > 2 {
|
|
t.Errorf("BStart = %.1f, want ≈ %.1f", r.BStart, wantBStart)
|
|
}
|
|
wantDur := 700 * ChromaprintSampleDur
|
|
if math.Abs(r.Duration-wantDur) > 4 {
|
|
t.Errorf("Duration = %.1f, want ≈ %.1f", r.Duration, wantDur)
|
|
}
|
|
}
|
|
|
|
func TestFindSharedRegion_NoMatchOnNoise(t *testing.T) {
|
|
ga, gb := &lcg{state: 7}, &lcg{state: 9}
|
|
a := make([]uint32, 1500)
|
|
b := make([]uint32, 1500)
|
|
for i := range a {
|
|
a[i] = ga.next()
|
|
b[i] = gb.next()
|
|
}
|
|
if r := FindSharedRegion(a, b, 15, 120); r != nil {
|
|
t.Fatalf("expected nil on unrelated noise, got %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestFindSharedRegion_FullMatchExceedsMaxDur(t *testing.T) {
|
|
// Two identical streams (same episode, two releases): the only region is
|
|
// the full window, which must be rejected by maxDur.
|
|
g := &lcg{state: 5}
|
|
a := make([]uint32, 2000)
|
|
for i := range a {
|
|
a[i] = g.next()
|
|
}
|
|
b := make([]uint32, 2000)
|
|
copy(b, a)
|
|
if r := FindSharedRegion(a, b, 15, 120); r != nil {
|
|
t.Fatalf("expected nil for identical streams (region > maxDur), got %+v", r)
|
|
}
|
|
}
|
|
|
|
func TestFindSharedRegion_ToleratesBitNoise(t *testing.T) {
|
|
// Same shared segment but with ≤2 flipped bits per point (re-encode noise).
|
|
shared := make([]uint32, 600)
|
|
g := &lcg{state: 77}
|
|
for i := range shared {
|
|
shared[i] = g.next()
|
|
}
|
|
noisy := make([]uint32, len(shared))
|
|
for i, v := range shared {
|
|
noisy[i] = v ^ (1 << uint(i%20)) // flip one bit
|
|
}
|
|
|
|
ga, gb := &lcg{state: 100}, &lcg{state: 200}
|
|
a := append(make([]uint32, 0, 1500), shared...)
|
|
for len(a) < 1500 {
|
|
a = append(a, ga.next())
|
|
}
|
|
b := append(make([]uint32, 0, 1500), noisy...)
|
|
for len(b) < 1500 {
|
|
b = append(b, gb.next())
|
|
}
|
|
|
|
r := FindSharedRegion(a, b, 15, 120)
|
|
if r == nil {
|
|
t.Fatal("expected match despite 1-bit noise, got nil")
|
|
}
|
|
if r.AStart > 2 {
|
|
t.Errorf("AStart = %.1f, want ≈ 0", r.AStart)
|
|
}
|
|
}
|