feat(library): detección de intro/créditos post-scan (skip segments)
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
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).
This commit is contained in:
parent
59da949a53
commit
a710bc1626
11 changed files with 1223 additions and 5 deletions
70
internal/library/skipdetect_test.go
Normal file
70
internal/library/skipdetect_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package library
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/torrentclaw/unarr/internal/library/mediainfo"
|
||||
)
|
||||
|
||||
func TestCreditsFromBlackRuns_DetectsCreditsRoll(t *testing.T) {
|
||||
// Movie of 7200s; credits-on-black from 6900 to the end, black frame every
|
||||
// ~2s, plus a stray fade-to-black at 5000s that must not win.
|
||||
var times []float64
|
||||
times = append(times, 5000, 5001) // short mid-film fade
|
||||
for tt := 6900.0; tt <= 7195; tt += 2 {
|
||||
times = append(times, tt)
|
||||
}
|
||||
segs := creditsFromBlackRuns(times, 7200)
|
||||
if len(segs) != 1 {
|
||||
t.Fatalf("expected 1 credits segment, got %d", len(segs))
|
||||
}
|
||||
s := segs[0]
|
||||
if s.Category != "credits" {
|
||||
t.Errorf("category = %q, want credits", s.Category)
|
||||
}
|
||||
if s.StartSec < 6890 || s.StartSec > 6910 {
|
||||
t.Errorf("StartSec = %.1f, want ≈ 6900", s.StartSec)
|
||||
}
|
||||
if s.EndSec != 7200 {
|
||||
t.Errorf("EndSec = %.1f, want 7200 (file end)", s.EndSec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreditsFromBlackRuns_RejectsShortFade(t *testing.T) {
|
||||
// Only a 20s black run near the end — too short to be credits.
|
||||
var times []float64
|
||||
for tt := 7170.0; tt <= 7190; tt += 2 {
|
||||
times = append(times, tt)
|
||||
}
|
||||
if segs := creditsFromBlackRuns(times, 7200); len(segs) != 0 {
|
||||
t.Fatalf("expected no segments for a 20s fade, got %+v", segs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreditsFromBlackRuns_RejectsRunNotReachingEnd(t *testing.T) {
|
||||
// 120s black run that ends 300s before the file end (a long mid-film
|
||||
// montage on black) — must not be flagged as credits.
|
||||
var times []float64
|
||||
for tt := 6700.0; tt <= 6820; tt += 2 {
|
||||
times = append(times, tt)
|
||||
}
|
||||
if segs := creditsFromBlackRuns(times, 7200); len(segs) != 0 {
|
||||
t.Fatalf("expected no segments when run stops mid-film, got %+v", segs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectForEpisode_PrefersDifferentEpisodePartners(t *testing.T) {
|
||||
// Sanity: an episode with no partners (all same episode number) yields nil.
|
||||
it := LibraryItem{FilePath: "/a/e1.mkv", Episode: 1, Season: 1}
|
||||
dup := LibraryItem{FilePath: "/a/e1-other-release.mkv", Episode: 1, Season: 1}
|
||||
fps := map[string]*episodeFingerprints{
|
||||
it.FilePath: {duration: 1400, intro: []uint32{1, 2, 3}, credits: []uint32{4, 5, 6}},
|
||||
dup.FilePath: {duration: 1400, intro: []uint32{1, 2, 3}, credits: []uint32{4, 5, 6}},
|
||||
}
|
||||
segs := detectForEpisode(it, fps[it.FilePath], []LibraryItem{it, dup}, fps)
|
||||
if len(segs) != 0 {
|
||||
t.Fatalf("expected no segments without a different-episode partner, got %+v", segs)
|
||||
}
|
||||
}
|
||||
|
||||
var _ = mediainfo.SkipSegmentRange{} // keep import for future use
|
||||
Loading…
Add table
Add a link
Reference in a new issue