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:
Deivid Soto 2026-06-10 17:39:09 +02:00
parent 4bdd161e02
commit 0dca296fec
6 changed files with 397 additions and 174 deletions

View file

@ -1,12 +1,74 @@
package library
import (
"context"
"path/filepath"
"strings"
"time"
"github.com/torrentclaw/unarr/internal/agent"
)
// SyncOptions describes ONE library sync session — a set of batches sharing a
// single syncStartedAt so the server can reap rows not seen by the session.
type SyncOptions struct {
AgentID string
// ScanPath is the primary root, kept for pre-scanRoots servers.
ScanPath string
// ScanRoots lists every root this session covers (see LibrarySyncRequest).
ScanRoots []string
// FullCycle: the session spans every configured root — the server may reap
// unseen rows regardless of path prefix. NEVER set it for a subtree scan.
FullCycle bool
// OnProgress, when non-nil, is called after each batch with (sent, total).
OnProgress func(sent, total int)
}
// SyncResult aggregates the per-batch server responses of a session.
type SyncResult struct {
Synced int
Matched int
Removed int
}
// SyncBatches uploads items to the server in batches of 100 as ONE sync
// session: every batch shares the same syncStartedAt and only the final one
// carries isLastBatch, so the server's stale-row cleanup sees the whole cycle
// at once. The single source of the batching protocol — shared by `unarr scan`
// (cmd/scan.go) and the daemon auto-scan (cmd/daemon.go); before this each
// root synced as its own session and the per-agent cleanup could reap rows of
// roots the session never visited.
func SyncBatches(ctx context.Context, ac *agent.Client, items []agent.LibrarySyncItem, opts SyncOptions) (SyncResult, error) {
const batchSize = 100
var res SyncResult
syncStartedAt := time.Now().UTC().Format(time.RFC3339)
for i := 0; i < len(items); i += batchSize {
end := i + batchSize
if end > len(items) {
end = len(items)
}
resp, err := ac.SyncLibrary(ctx, agent.LibrarySyncRequest{
Items: items[i:end],
ScanPath: opts.ScanPath,
AgentID: opts.AgentID,
IsLastBatch: end >= len(items),
SyncStartedAt: syncStartedAt,
ScanRoots: opts.ScanRoots,
FullCycle: opts.FullCycle,
})
if err != nil {
return res, err
}
res.Synced += resp.Synced
res.Matched += resp.Matched
res.Removed += resp.Removed
if opts.OnProgress != nil {
opts.OnProgress(end, len(items))
}
}
return res, nil
}
// relToRoot returns the file's path relative to the scan root (forward-slashed),
// or "" when it doesn't live under root. The server stores this so streaming can
// later reconstruct the absolute path from the agent's *current* root.