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

@ -131,6 +131,32 @@ func (c *Client) MarkSessionReady(ctx context.Context, sessionID string, health
return nil
}
// ReportSessionError is the failure-path counterpart of MarkSessionReady: it
// tells the web a streaming session can NOT start (file gone, path rejected,
// ffmpeg missing, spawn failure…). The web marks the session failed, pushes an
// SSE "failed" event so the player stops probing a playlist that will never
// exist, and self-heals stale library state on code "file_missing".
//
// code is one of the stable machine codes the web understands:
// "file_missing" | "path_rejected" | "no_video_file" | "ffmpeg_unavailable" |
// "start_failed". message is free-form detail for diagnostics.
//
// Best-effort like MarkSessionReady: on older web deployments without the
// endpoint this 404s — the caller logs and the player falls back to its
// probe-deadline behaviour, exactly as before this channel existed.
func (c *Client) ReportSessionError(ctx context.Context, sessionID, code, message string) error {
req := struct {
SessionID string `json:"sessionId"`
Code string `json:"code"`
Message string `json:"message,omitempty"`
}{SessionID: sessionID, Code: code, Message: message}
var resp StatusResponse
if err := c.doPost(ctx, "/api/internal/agent/session-error", req, &resp); err != nil {
return fmt.Errorf("report session error: %w", err)
}
return nil
}
// SessionHealth is an OPTIONAL live-transcode health snapshot attached to a
// session-ready report (F3). A nil *SessionHealth means the agent has no
// telemetry to share (cache hit, direct-play, or progress not yet stable) and

View file

@ -361,6 +361,17 @@ type LibrarySyncRequest struct {
AgentID string `json:"agentId,omitempty"` // lets the server scope stale-cleanup per agent
IsLastBatch bool `json:"isLastBatch"`
SyncStartedAt string `json:"syncStartedAt,omitempty"` // ISO-8601; same for all batches in a session
// ScanRoots lists EVERY root this sync session covered (a session spans all
// roots since 1.0.9 — one syncStartedAt, one isLastBatch). The server scopes
// stale-row cleanup of a partial session to these prefixes. Older servers
// ignore the field and fall back to ScanPath.
ScanRoots []string `json:"scanRoots,omitempty"`
// FullCycle marks a session that covered every root the agent scans
// (daemon auto-scan, `unarr scan` without args). The server may then reap
// unseen rows REGARDLESS of path prefix — old-base-path ghost rows
// included. Must stay false for a manual subtree scan or when any root's
// scan failed, or the cleanup would reap rows the session never visited.
FullCycle bool `json:"fullCycle,omitempty"`
}
// LibrarySyncItem is a single scanned media file with ffprobe metadata.