From a5a92b111bbea6466e4362cedba793f27892250b Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Sat, 23 May 2026 15:36:37 +0200 Subject: [PATCH] feat(usenet): warn at startup when par2 or extractor is missing A usenet-enabled agent silently produces corrupt files when par2 is not installed: bad NNTP segments go unrepaired and unrar reports checksum errors. Likewise, no unrar/7z means RAR-packed downloads can't be unpacked at all. When the registered agent has the usenet feature, check par2 and the extractor (unrar/7z) in PATH and log a loud WARNING for each missing one, mirroring the existing ffmpeg-for-HLS warning. --- internal/agent/daemon.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/agent/daemon.go b/internal/agent/daemon.go index a8edc9b..385454a 100644 --- a/internal/agent/daemon.go +++ b/internal/agent/daemon.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "os/exec" "runtime" "strings" "sync/atomic" @@ -178,6 +179,21 @@ func (d *Daemon) Run(ctx context.Context) error { log.Printf("Agent registered: %s (%s) [%s]", d.User.Name, d.User.Email, d.User.Plan) log.Printf("Features: torrent=%v debrid=%v usenet=%v", d.Features.Torrent, d.Features.Debrid, d.Features.Usenet) + // Usenet needs par2 (segment repair) + an extractor (RAR/7z) on the host. + // Without par2, a single bad segment corrupts the file silently; without + // an extractor, RAR-packed downloads can't be unpacked. Warn loudly at + // startup so the operator installs them before the first download fails. + if d.Features.Usenet { + if _, err := exec.LookPath("par2"); err != nil { + log.Printf("[usenet] WARNING: par2 not found in PATH — corrupted segments cannot be repaired and extraction may fail. Install par2 (apt install par2 / brew install par2).") + } + _, unrarErr := exec.LookPath("unrar") + _, sevenZErr := exec.LookPath("7z") + if unrarErr != nil && sevenZErr != nil { + log.Printf("[usenet] WARNING: no archive extractor (unrar or 7z) found — RAR-packed downloads cannot be unpacked. Install unrar or 7z.") + } + } + // Wire sync callbacks d.sync.OnNewTasks = func(tasks []Task) { if d.OnTasksClaimed != nil {