feat(stream): debrid passthrough for mode=stream tasks (external players)

handleStreamTask now serves a mode=stream task FROM a resolved debrid HTTPS link
(when the web set preferredMethod=debrid + the torrent is cached) instead of
joining the P2P swarm — served over the SAME /stream endpoint so VLC and other
external players consume it identically (and far faster). No HLS transcode:
external players handle any container. Falls through to the P2P StreamEngine
when there is no direct URL. Uses the mutex-safe SetStreamURL setter.

Also widen the debrid HEAD size-probe timeout 10s -> 15s to match the transport's
TLS handshake budget, so a slow CDN no longer trips it and falls back to a
guessed size.

Bump 1.0.2-beta.
This commit is contained in:
Deivid Soto 2026-06-03 22:43:43 +02:00
parent 8e37293b7d
commit aba20e2078
4 changed files with 64 additions and 2 deletions

View file

@ -91,6 +91,13 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
// NOTE: we deliberately do NOT cancel prior stream goroutines here. The
// persistent StreamServer is last-writer-wins (SetFile replaces the file;
// the deferred ClearFile is guarded by CurrentTaskID), so a displaced prior
// goroutine simply parks on its own ctx until the 30m idle guard reaps it —
// cheap. Cancelling them at entry would abort an in-flight debrid HEAD of a
// concurrently-starting task (size resolution), failing that stream.
// Register for web-initiated cancellation
streamRegistry.mu.Lock()
streamRegistry.cancels[at.ID] = cancel
@ -114,6 +121,47 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
reporter.Track(task)
defer reporter.ReportFinal(context.Background(), task)
// Debrid passthrough: when the web resolved a direct HTTPS link (the torrent
// is cached on the user's debrid + preferredMethod=debrid), stream FROM that
// link instead of joining the P2P swarm — served over the SAME /stream
// endpoint, so VLC / external players consume it identically (and far
// faster). No HLS transcode here: external players handle any container.
// Falls through to the P2P StreamEngine below when there is no direct URL.
if at.DirectURL != "" {
task.ResolvedMethod = engine.MethodDebrid
task.Transition(engine.StatusResolving)
bctx, bcancel := context.WithTimeout(ctx, 15*time.Second)
// fallbackSize 0 → provider derives size from a HEAD; refresh nil → no
// task-level link-refresh endpoint exists (the web re-resolves stale
// debrid URLs at the next claim). A mid-stream expiry just ends the
// stream and the user re-opens it.
provider, perr := engine.NewDebridFileProvider(bctx, at.DirectURL, at.DirectFileName, 0, nil)
bcancel()
if perr != nil {
task.ErrorMessage = "debrid stream provider: " + perr.Error()
task.Transition(engine.StatusFailed)
return
}
srv.SetFile(provider, at.ID)
task.FileName = provider.FileName()
task.TotalBytes = provider.FileSize()
task.SetStreamURL(srv.URLsJSON()) // mutex-safe: the reporter reads it via GetStreamURL
log.Printf("[%s] stream (debrid): %s (%s) url: %s", at.ID[:8], provider.FileName(), ui.FormatBytes(provider.FileSize()), srv.URL())
if agentClient != nil {
watchReporter := engine.NewWatchReporter(agentClient, srv, at.ID)
go watchReporter.Run(ctx)
}
// Debrid serves a complete remote file — there is no download to track,
// so mark it complete immediately (the UI shows "ready"). The persistent
// server keeps serving until the idle guard reaps it (30m), same as P2P.
task.Transition(engine.StatusCompleted)
<-ctx.Done()
log.Printf("[%s] stream (debrid) stopped", at.ID[:8])
return
}
// 1. Create StreamEngine
eng, err := engine.NewStreamEngine(engine.StreamConfig{
DataDir: cfg.Download.Dir,

View file

@ -1,4 +1,4 @@
package cmd
// Version is the CLI version. Overridden by goreleaser ldflags at release time.
var Version = "1.0.1-beta"
var Version = "1.0.2-beta"