feat(downloads): ordered preferred_methods list honored for web tasks

The agent ignored its config.toml method preference for web-driven downloads
(only the local `unarr download` command read it), and resolveMethod tried
torrent first in auto mode — so a 'debrid only' user still got torrent tasks.

- config: preferred_methods (ordered list, e.g. ["debrid","usenet"]) with
  MethodOrder() resolution; back-compat with the singular preferred_method.
  Methods absent from the list are disabled (debrid-only never torrents).
- resolveMethod/tryFallback honor the config order (gating, no fallback to a
  method outside the list) over the per-task preference.
- report preferred_methods on register so the web honors it (resolves debrid,
  gates the P2P stream fallback).
- enable the usenet downloader when usenet is listed (it was never enabled).
- config_menu: ordered presets (debrid-only, debrid→torrent, debrid→usenet…).

Tests: resolveMethod gating + fallback within/outside the list.
This commit is contained in:
Deivid Soto 2026-06-14 12:51:32 +02:00
parent 523ecc724a
commit c7ee0c0a28
8 changed files with 256 additions and 45 deletions

View file

@ -157,9 +157,21 @@ func configDownloads(cfg *config.Config) error {
concurrent = "3"
}
validMethods := map[string]bool{"auto": true, "torrent": true, "debrid": true, "usenet": true}
if !validMethods[cfg.Download.PreferredMethod] {
cfg.Download.PreferredMethod = "auto"
// Method preference is an ordered list (PreferredMethods). The menu exposes
// the common presets as a single choice; custom orders can still be hand-set
// in config.toml. Derive the current preset from the effective order.
methodPreset := "auto"
switch strings.Join(cfg.Download.MethodOrder(), ",") {
case "torrent":
methodPreset = "torrent"
case "debrid":
methodPreset = "debrid"
case "usenet":
methodPreset = "usenet"
case "debrid,torrent":
methodPreset = "debrid,torrent"
case "debrid,usenet":
methodPreset = "debrid,usenet"
}
validQualities := map[string]bool{"": true, "720p": true, "1080p": true, "2160p": true}
@ -174,13 +186,16 @@ func configDownloads(cfg *config.Config) error {
Value(&cfg.Download.Dir),
huh.NewSelect[string]().
Title("Preferred method").
Description("Methods not listed are disabled (e.g. debrid-only never uses torrent)").
Options(
huh.NewOption("Auto (torrent + debrid when available)", "auto"),
huh.NewOption("Auto (web decides — torrent + debrid when available)", "auto"),
huh.NewOption("Torrent only (BitTorrent P2P)", "torrent"),
huh.NewOption("Debrid only (Real-Debrid, AllDebrid...)", "debrid"),
huh.NewOption("Usenet only (requires Pro)", "usenet"),
huh.NewOption("Debrid, then torrent", "debrid,torrent"),
huh.NewOption("Debrid, then usenet (requires Pro)", "debrid,usenet"),
).
Value(&cfg.Download.PreferredMethod),
Value(&methodPreset),
huh.NewSelect[string]().
Title("Preferred quality").
Description("Hint for automatic torrent selection").
@ -225,6 +240,15 @@ func configDownloads(cfg *config.Config) error {
if n > 0 {
cfg.Download.MaxConcurrent = n
}
// Persist the preset as the ordered list (source of truth). "auto" clears the
// list; legacy PreferredMethod is kept in sync so an old reader still works.
if methodPreset == "auto" {
cfg.Download.PreferredMethods = nil
cfg.Download.PreferredMethod = "auto"
} else {
cfg.Download.PreferredMethods = strings.Split(methodPreset, ",")
cfg.Download.PreferredMethod = cfg.Download.PreferredMethods[0]
}
return nil
}

View file

@ -248,6 +248,7 @@ func runDaemonStart() error {
HWDevices: hwDiag.Devices,
AutoUpgrade: cfg.Daemon.AutoUpgradeEnabled(),
Downlink: cfg.Daemon.Downlink,
PreferredMethods: cfg.Download.MethodOrder(),
}
// Create HTTP client with mirror failover so a `.com` block-out rolls
@ -381,6 +382,17 @@ func runDaemonStart() error {
// Create debrid downloader
debridDl := engine.NewDebridDownloader()
usenetDl := engine.NewUsenetDownloader(agentClient)
// Enable usenet when the user explicitly lists it in preferred_methods — the
// downloader gates on this flag, so without it a "usenet" preference would
// resolve to nothing. (Auto users keep the historical behaviour.)
methodOrder := cfg.Download.MethodOrder()
for _, m := range methodOrder {
if m == "usenet" {
usenetDl.SetEnabled(true)
log.Printf("[usenet] enabled via preferred_methods")
break
}
}
// Pre-flight disk reserve: refuse a download that would leave less than this
// many bytes free, so a download never fills the filesystem to 0 mid-write.
@ -392,9 +404,10 @@ func runDaemonStart() error {
// Create download manager
manager := engine.NewManager(engine.ManagerConfig{
MaxConcurrent: cfg.Download.MaxConcurrent,
OutputDir: cfg.Download.Dir,
Notifications: cfg.Notifications.Enabled,
MaxConcurrent: cfg.Download.MaxConcurrent,
OutputDir: cfg.Download.Dir,
Notifications: cfg.Notifications.Enabled,
PreferredMethods: methodOrder,
Organize: engine.OrganizeConfig{
Enabled: cfg.Organize.Enabled,
MoviesDir: cfg.Organize.MoviesDir,