- 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ó
98 lines
3.2 KiB
Go
98 lines
3.2 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|