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

@ -6,21 +6,47 @@ import (
"log"
)
// resolveMethod determines which download method to use for a task.
// For "auto": tries available methods in priority order (torrent > debrid > usenet).
// For specific method: uses only that method.
func resolveMethod(ctx context.Context, task *Task, downloaders map[DownloadMethod]Downloader) (DownloadMethod, error) {
var order []DownloadMethod
// effectiveOrder returns the ordered methods to try for a task.
//
// The agent's local config (configMethods, from config.toml `preferred_methods`)
// WINS and gates: only the listed methods are eligible, in that order — so a
// "debrid only" agent never tries torrent even if the web's task says otherwise.
// When the config has no explicit preference (nil), we fall back to the per-task
// preference the web sent: a specific method runs alone; "auto" tries all three
// torrent-first (the historical default).
func effectiveOrder(task *Task, configMethods []string) []DownloadMethod {
if len(configMethods) > 0 {
order := make([]DownloadMethod, 0, len(configMethods))
for _, m := range configMethods {
switch m {
case "torrent":
order = append(order, MethodTorrent)
case "debrid":
order = append(order, MethodDebrid)
case "usenet":
order = append(order, MethodUsenet)
}
}
if len(order) > 0 {
return order
}
}
switch task.PreferredMethod {
case "torrent":
order = []DownloadMethod{MethodTorrent}
return []DownloadMethod{MethodTorrent}
case "debrid":
order = []DownloadMethod{MethodDebrid}
return []DownloadMethod{MethodDebrid}
case "usenet":
order = []DownloadMethod{MethodUsenet}
return []DownloadMethod{MethodUsenet}
default: // "auto"
order = []DownloadMethod{MethodTorrent, MethodDebrid, MethodUsenet}
return []DownloadMethod{MethodTorrent, MethodDebrid, MethodUsenet}
}
}
// resolveMethod determines which download method to use for a task, honouring the
// agent's configured method order (gating) over the per-task preference.
func resolveMethod(ctx context.Context, task *Task, downloaders map[DownloadMethod]Downloader, configMethods []string) (DownloadMethod, error) {
order := effectiveOrder(task, configMethods)
for _, method := range order {
// Skip already-tried methods
@ -54,22 +80,34 @@ func resolveMethod(ctx context.Context, task *Task, downloaders map[DownloadMeth
}
}
return "", fmt.Errorf("no download method available (tried: %v)", task.TriedMethods)
return "", fmt.Errorf("no download method available (order: %v, tried: %v)", order, task.TriedMethods)
}
// tryFallback attempts to fall back to the next untried download method.
// Returns true if fallback was initiated, false if no more methods.
func tryFallback(task *Task, downloaders map[DownloadMethod]Downloader) bool {
if task.PreferredMethod != "auto" {
return false // specific method requested, no fallback
// tryFallback attempts to fall back to the next untried download method WITHIN
// the effective order. A single-method order (e.g. "debrid only") has no
// fallback — failing over to torrent would defeat the whole preference.
func tryFallback(task *Task, downloaders map[DownloadMethod]Downloader, configMethods []string) bool {
order := effectiveOrder(task, configMethods)
if len(order) <= 1 {
return false // single method requested, no fallback
}
task.TriedMethods = append(task.TriedMethods, task.ResolvedMethod)
available := make([]DownloadMethod, 0, len(downloaders))
for m := range downloaders {
available = append(available, m)
for _, m := range order {
tried := false
for _, tm := range task.TriedMethods {
if tm == m {
tried = true
break
}
}
if tried {
continue
}
if _, ok := downloaders[m]; ok {
return true
}
}
return task.HasUntried(available)
return false
}