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:
parent
8e37293b7d
commit
aba20e2078
4 changed files with 64 additions and 2 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.2-beta] - 2026-06-03
|
||||||
|
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **stream**: serve a `mode=stream` task from a debrid HTTPS link when the torrent is cached (debrid passthrough for external players / VLC), falling back to P2P stream-while-download when it isn't
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **stream**: widen the debrid HEAD size-probe timeout to 15s to match the TLS handshake budget — a slow CDN no longer trips the old 10s and falls back to a guessed size
|
||||||
|
|
||||||
## [1.0.1-beta] - 2026-06-03
|
## [1.0.1-beta] - 2026-06-03
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,13 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
|
||||||
ctx, cancel := context.WithCancel(parentCtx)
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
defer cancel()
|
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
|
// Register for web-initiated cancellation
|
||||||
streamRegistry.mu.Lock()
|
streamRegistry.mu.Lock()
|
||||||
streamRegistry.cancels[at.ID] = cancel
|
streamRegistry.cancels[at.ID] = cancel
|
||||||
|
|
@ -114,6 +121,47 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
|
||||||
reporter.Track(task)
|
reporter.Track(task)
|
||||||
defer reporter.ReportFinal(context.Background(), 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
|
// 1. Create StreamEngine
|
||||||
eng, err := engine.NewStreamEngine(engine.StreamConfig{
|
eng, err := engine.NewStreamEngine(engine.StreamConfig{
|
||||||
DataDir: cfg.Download.Dir,
|
DataDir: cfg.Download.Dir,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
// Version is the CLI version. Overridden by goreleaser ldflags at release time.
|
// Version is the CLI version. Overridden by goreleaser ldflags at release time.
|
||||||
var Version = "1.0.1-beta"
|
var Version = "1.0.2-beta"
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,10 @@ func (r *debridRangeReader) reopen() error {
|
||||||
// size the web reported. A short timeout keeps a slow/HEAD-hostile CDN from
|
// size the web reported. A short timeout keeps a slow/HEAD-hostile CDN from
|
||||||
// stalling session setup — the fallback size is good enough to start.
|
// stalling session setup — the fallback size is good enough to start.
|
||||||
func debridHeadSize(ctx context.Context, url string) (int64, bool) {
|
func debridHeadSize(ctx context.Context, url string) (int64, bool) {
|
||||||
hctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
// 15s (not 10s): the transport's TLS handshake budget alone is 15s, so a
|
||||||
|
// slow debrid CDN could trip the old 10s timeout before headers arrived,
|
||||||
|
// needlessly falling back to a guessed size.
|
||||||
|
hctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
req, err := http.NewRequestWithContext(hctx, http.MethodHead, url, nil)
|
req, err := http.NewRequestWithContext(hctx, http.MethodHead, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue