fix(daemon): reportar fallos de arranque de sesión a la web + scan en sesión única
- nuevo agentClient.ReportSessionError → POST /agent/session-error; failSession() en todos los abortos del handler de sesiones (path muerto, ffmpeg ausente, remux, provider debrid, StartHLSSession). Antes eran returns mudos y el player quedaba en "Preparando sesión" hasta agotar el deadline de probes - resolvePlayableFile() unifica la resolución de paths del /stream raw y de las sesiones HLS/remux/direct (remap de base path + stat con retries NFS + directorio→vídeo, antes duplicada y divergente) y distingue file_missing (la web self-heala filas stale) de path_rejected (el fichero existe fuera de los roots = config; la web no debe podar nada) - library.SyncBatches: el batching del sync de biblioteca vive en un solo sitio; el scan manual y el auto-scan sincronizan todos los roots en UNA sesión con scanRoots/fullCycle, en vez de una sesión por root que dejaba al server podar filas de roots que la sesión nunca visitó
This commit is contained in:
parent
4bdd161e02
commit
0dca296fec
6 changed files with 397 additions and 174 deletions
98
internal/cmd/resolve_playable_test.go
Normal file
98
internal/cmd/resolve_playable_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolvePlayableFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkfile(t, filepath.Join(root, "Acme Show", "Season 01", "ep.mkv"))
|
||||
roots := []string{root}
|
||||
|
||||
t.Run("allowed path resolves to itself", func(t *testing.T) {
|
||||
want := filepath.Join(root, "Acme Show", "Season 01", "ep.mkv")
|
||||
got, code, err := resolvePlayableFile(want, roots, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (%s): %v", code, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("old base path relocates onto current root", func(t *testing.T) {
|
||||
got, code, err := resolvePlayableFile("/old/base/Acme Show/Season 01/ep.mkv", roots, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (%s): %v", code, err)
|
||||
}
|
||||
want := filepath.Join(root, "Acme Show", "Season 01", "ep.mkv")
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("deleted file under old base is file_missing, never path_rejected", func(t *testing.T) {
|
||||
// The incident shape (2026-06-10): web hands a stale host path
|
||||
// (/mnt/nas/…) whose file was deleted — the docker agent can't see the
|
||||
// original path AND no tail relocates. file_missing tells the web to
|
||||
// prune the stale row; path_rejected would block that self-heal.
|
||||
_, code, err := resolvePlayableFile("/old/base/Acme Show/Season 01/gone.mkv", roots, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for deleted file")
|
||||
}
|
||||
if code != pathErrMissing {
|
||||
t.Errorf("code = %q, want %q", code, pathErrMissing)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("existing file outside roots is path_rejected", func(t *testing.T) {
|
||||
outside := t.TempDir()
|
||||
// 1-segment-deep on purpose: a ≥3-segment tail could legitimately
|
||||
// relocate INTO the root if a same-named file existed there.
|
||||
mkfile(t, filepath.Join(outside, "leak.mkv"))
|
||||
_, code, err := resolvePlayableFile(filepath.Join(outside, "leak.mkv"), roots, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for out-of-root file")
|
||||
}
|
||||
if code != pathErrRejected {
|
||||
t.Errorf("code = %q, want %q", code, pathErrRejected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing file inside an allowed root is file_missing", func(t *testing.T) {
|
||||
_, code, err := resolvePlayableFile(filepath.Join(root, "Acme Show", "Season 01", "gone.mkv"), roots, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
if code != pathErrMissing {
|
||||
t.Errorf("code = %q, want %q", code, pathErrMissing)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory resolves to its video file", func(t *testing.T) {
|
||||
got, code, err := resolvePlayableFile(filepath.Join(root, "Acme Show", "Season 01"), roots, "test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (%s): %v", code, err)
|
||||
}
|
||||
want := filepath.Join(root, "Acme Show", "Season 01", "ep.mkv")
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory without video is no_video_file", func(t *testing.T) {
|
||||
empty := filepath.Join(root, "Empty Show")
|
||||
if err := os.MkdirAll(empty, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, code, err := resolvePlayableFile(empty, roots, "test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty directory")
|
||||
}
|
||||
if code != pathErrNoVideo {
|
||||
t.Errorf("code = %q, want %q", code, pathErrNoVideo)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue