Merge feat/preferred-methods-list: ordered download-method preference
This commit is contained in:
commit
16253ec106
8 changed files with 256 additions and 45 deletions
|
|
@ -41,6 +41,9 @@ type DaemonConfig struct {
|
||||||
HWDevices []string // device files + driver bins detected at probe time
|
HWDevices []string // device files + driver bins detected at probe time
|
||||||
AutoUpgrade bool // honor server-flagged upgrades by downloading + restarting (default: true)
|
AutoUpgrade bool // honor server-flagged upgrades by downloading + restarting (default: true)
|
||||||
Downlink string // realtime downlink transport: "auto" (SSE+long-poll fallback) | "sse" | "poll"
|
Downlink string // realtime downlink transport: "auto" (SSE+long-poll fallback) | "sse" | "poll"
|
||||||
|
// PreferredMethods is the ordered download-method preference from config.toml
|
||||||
|
// (e.g. ["debrid","usenet"]). Reported to the web so it honours the gating.
|
||||||
|
PreferredMethods []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon manages agent registration and the sync loop.
|
// Daemon manages agent registration and the sync loop.
|
||||||
|
|
@ -165,6 +168,7 @@ func (d *Daemon) Register(ctx context.Context) error {
|
||||||
VPNServer: d.vpnServer,
|
VPNServer: d.vpnServer,
|
||||||
FunnelURL: d.funnelURL,
|
FunnelURL: d.funnelURL,
|
||||||
IsDocker: RunningInDocker(),
|
IsDocker: RunningInDocker(),
|
||||||
|
PreferredMethods: d.cfg.PreferredMethods,
|
||||||
}
|
}
|
||||||
if free, total, err := DiskInfo(d.cfg.DownloadDir); err == nil {
|
if free, total, err := DiskInfo(d.cfg.DownloadDir); err == nil {
|
||||||
req.DiskFreeBytes = free
|
req.DiskFreeBytes = free
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,10 @@ type RegisterRequest struct {
|
||||||
// self-update refuses to run in Docker). No omitempty: false (a binary
|
// self-update refuses to run in Docker). No omitempty: false (a binary
|
||||||
// install) is a meaningful state the server must see to keep the button.
|
// install) is a meaningful state the server must see to keep the button.
|
||||||
IsDocker bool `json:"isDocker"`
|
IsDocker bool `json:"isDocker"`
|
||||||
|
// PreferredMethods is the agent's ordered download-method preference from
|
||||||
|
// config.toml (e.g. ["debrid","usenet"]). The web honours it so a "debrid
|
||||||
|
// only" agent is never handed a torrent task. Empty/["auto"] → web decides.
|
||||||
|
PreferredMethods []string `json:"preferredMethods,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterResponse is returned by the server after registration.
|
// RegisterResponse is returned by the server after registration.
|
||||||
|
|
|
||||||
|
|
@ -157,9 +157,21 @@ func configDownloads(cfg *config.Config) error {
|
||||||
concurrent = "3"
|
concurrent = "3"
|
||||||
}
|
}
|
||||||
|
|
||||||
validMethods := map[string]bool{"auto": true, "torrent": true, "debrid": true, "usenet": true}
|
// Method preference is an ordered list (PreferredMethods). The menu exposes
|
||||||
if !validMethods[cfg.Download.PreferredMethod] {
|
// the common presets as a single choice; custom orders can still be hand-set
|
||||||
cfg.Download.PreferredMethod = "auto"
|
// 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}
|
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),
|
Value(&cfg.Download.Dir),
|
||||||
huh.NewSelect[string]().
|
huh.NewSelect[string]().
|
||||||
Title("Preferred method").
|
Title("Preferred method").
|
||||||
|
Description("Methods not listed are disabled (e.g. debrid-only never uses torrent)").
|
||||||
Options(
|
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("Torrent only (BitTorrent P2P)", "torrent"),
|
||||||
huh.NewOption("Debrid only (Real-Debrid, AllDebrid...)", "debrid"),
|
huh.NewOption("Debrid only (Real-Debrid, AllDebrid...)", "debrid"),
|
||||||
huh.NewOption("Usenet only (requires Pro)", "usenet"),
|
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]().
|
huh.NewSelect[string]().
|
||||||
Title("Preferred quality").
|
Title("Preferred quality").
|
||||||
Description("Hint for automatic torrent selection").
|
Description("Hint for automatic torrent selection").
|
||||||
|
|
@ -225,6 +240,15 @@ func configDownloads(cfg *config.Config) error {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
cfg.Download.MaxConcurrent = n
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ func runDaemonStart() error {
|
||||||
HWDevices: hwDiag.Devices,
|
HWDevices: hwDiag.Devices,
|
||||||
AutoUpgrade: cfg.Daemon.AutoUpgradeEnabled(),
|
AutoUpgrade: cfg.Daemon.AutoUpgradeEnabled(),
|
||||||
Downlink: cfg.Daemon.Downlink,
|
Downlink: cfg.Daemon.Downlink,
|
||||||
|
PreferredMethods: cfg.Download.MethodOrder(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP client with mirror failover so a `.com` block-out rolls
|
// Create HTTP client with mirror failover so a `.com` block-out rolls
|
||||||
|
|
@ -381,6 +382,17 @@ func runDaemonStart() error {
|
||||||
// Create debrid downloader
|
// Create debrid downloader
|
||||||
debridDl := engine.NewDebridDownloader()
|
debridDl := engine.NewDebridDownloader()
|
||||||
usenetDl := engine.NewUsenetDownloader(agentClient)
|
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
|
// 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.
|
// many bytes free, so a download never fills the filesystem to 0 mid-write.
|
||||||
|
|
@ -392,9 +404,10 @@ func runDaemonStart() error {
|
||||||
|
|
||||||
// Create download manager
|
// Create download manager
|
||||||
manager := engine.NewManager(engine.ManagerConfig{
|
manager := engine.NewManager(engine.ManagerConfig{
|
||||||
MaxConcurrent: cfg.Download.MaxConcurrent,
|
MaxConcurrent: cfg.Download.MaxConcurrent,
|
||||||
OutputDir: cfg.Download.Dir,
|
OutputDir: cfg.Download.Dir,
|
||||||
Notifications: cfg.Notifications.Enabled,
|
Notifications: cfg.Notifications.Enabled,
|
||||||
|
PreferredMethods: methodOrder,
|
||||||
Organize: engine.OrganizeConfig{
|
Organize: engine.OrganizeConfig{
|
||||||
Enabled: cfg.Organize.Enabled,
|
Enabled: cfg.Organize.Enabled,
|
||||||
MoviesDir: cfg.Organize.MoviesDir,
|
MoviesDir: cfg.Organize.MoviesDir,
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,21 @@ type AgentConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadConfig struct {
|
type DownloadConfig struct {
|
||||||
Dir string `toml:"dir"`
|
Dir string `toml:"dir"`
|
||||||
PreferredMethod string `toml:"preferred_method"`
|
// PreferredMethod (singular, legacy) — kept for back-compat. A single
|
||||||
PreferredQuality string `toml:"preferred_quality"` // "2160p", "1080p", "720p" — hint for auto-selection
|
// "auto"|"torrent"|"debrid"|"usenet". Superseded by PreferredMethods.
|
||||||
MaxConcurrent int `toml:"max_concurrent"`
|
PreferredMethod string `toml:"preferred_method"`
|
||||||
MinFreeDiskMB int `toml:"min_free_disk_mb"` // refuse a download if it would leave less than this free (reserve to keep the FS healthy); default 2048, 0 = disable
|
// PreferredMethods (ordered list) is the source of truth when set, e.g.
|
||||||
MaxDownloadSpeed string `toml:"max_download_speed"` // e.g. "10MB", "500KB", "0" = unlimited
|
// ["debrid","usenet"] = try debrid, then usenet, and DISABLE torrent (it's
|
||||||
MaxUploadSpeed string `toml:"max_upload_speed"` // e.g. "1MB", "0" = unlimited
|
// not in the list). ["auto"] or empty → defer to the web policy. The web
|
||||||
|
// honours this (reported on register) so a "debrid only" agent never gets a
|
||||||
|
// torrent task it didn't ask for. See MethodOrder() for resolution.
|
||||||
|
PreferredMethods []string `toml:"preferred_methods"`
|
||||||
|
PreferredQuality string `toml:"preferred_quality"` // "2160p", "1080p", "720p" — hint for auto-selection
|
||||||
|
MaxConcurrent int `toml:"max_concurrent"`
|
||||||
|
MinFreeDiskMB int `toml:"min_free_disk_mb"` // refuse a download if it would leave less than this free (reserve to keep the FS healthy); default 2048, 0 = disable
|
||||||
|
MaxDownloadSpeed string `toml:"max_download_speed"` // e.g. "10MB", "500KB", "0" = unlimited
|
||||||
|
MaxUploadSpeed string `toml:"max_upload_speed"` // e.g. "1MB", "0" = unlimited
|
||||||
// Seeding lifecycle (BitTorrent only). Off by default — the daemon leeches
|
// Seeding lifecycle (BitTorrent only). Off by default — the daemon leeches
|
||||||
// then drops the torrent. Enable to keep uploading after a download finishes;
|
// then drops the torrent. Enable to keep uploading after a download finishes;
|
||||||
// seeding stops at whichever target is hit first, or never if both are unset.
|
// seeding stops at whichever target is hit first, or never if both are unset.
|
||||||
|
|
@ -256,6 +264,46 @@ func (t TrickplayConfig) IntervalSeconds() float64 {
|
||||||
return 10
|
return 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validMethod reports whether s is a known download backend.
|
||||||
|
func validMethod(s string) bool {
|
||||||
|
return s == "torrent" || s == "debrid" || s == "usenet"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodOrder returns the effective ordered download-method preference, or nil
|
||||||
|
// for "auto" (defer to the web policy / torrent-first fallback). PreferredMethods
|
||||||
|
// (the list) wins; the legacy singular PreferredMethod is the fallback. "auto"
|
||||||
|
// anywhere collapses to nil. Unknown entries are dropped, dupes removed, order
|
||||||
|
// preserved. A nil/empty result means "no explicit preference".
|
||||||
|
func (c DownloadConfig) MethodOrder() []string {
|
||||||
|
src := c.PreferredMethods
|
||||||
|
if len(src) == 0 && c.PreferredMethod != "" {
|
||||||
|
src = []string{c.PreferredMethod}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(src))
|
||||||
|
for _, m := range src {
|
||||||
|
m = strings.ToLower(strings.TrimSpace(m))
|
||||||
|
if m == "auto" {
|
||||||
|
return nil // auto anywhere → defer
|
||||||
|
}
|
||||||
|
if validMethod(m) && !contains(out, m) {
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s []string, v string) bool {
|
||||||
|
for _, x := range s {
|
||||||
|
if x == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Default returns a Config with sensible defaults. Used both for fresh
|
// Default returns a Config with sensible defaults. Used both for fresh
|
||||||
// installs (no config file yet) and as the baseline for Load — fields not
|
// installs (no config file yet) and as the baseline for Load — fields not
|
||||||
// present in the user's TOML keep their Default() value.
|
// present in the user's TOML keep their Default() value.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ type ManagerConfig struct {
|
||||||
OutputDir string
|
OutputDir string
|
||||||
Organize OrganizeConfig
|
Organize OrganizeConfig
|
||||||
Notifications bool // send desktop notifications on complete/fail
|
Notifications bool // send desktop notifications on complete/fail
|
||||||
|
// PreferredMethods is the agent's ordered download-method preference from
|
||||||
|
// config.toml (e.g. ["debrid","usenet"]). Non-empty → it gates which methods
|
||||||
|
// resolveMethod will try, ignoring the per-task preference. Empty/nil → defer
|
||||||
|
// to the task's web-sent preference (legacy auto/torrent-first).
|
||||||
|
PreferredMethods []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager orchestrates concurrent downloads with method resolution and fallback.
|
// Manager orchestrates concurrent downloads with method resolution and fallback.
|
||||||
|
|
@ -380,7 +385,7 @@ func (m *Manager) processTask(ctx context.Context, task *Task) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
method, err := resolveMethod(ctx, task, m.downloaders)
|
method, err := resolveMethod(ctx, task, m.downloaders, m.cfg.PreferredMethods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.fail(ctx, task, "no method available: "+err.Error())
|
m.fail(ctx, task, "no method available: "+err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -416,7 +421,7 @@ func (m *Manager) processTask(ctx context.Context, task *Task) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Try fallback
|
// Try fallback
|
||||||
if tryFallback(task, m.downloaders) {
|
if tryFallback(task, m.downloaders, m.cfg.PreferredMethods) {
|
||||||
log.Printf("[%s] %s failed, trying fallback: %v", agent.ShortID(task.ID), method, err)
|
log.Printf("[%s] %s failed, trying fallback: %v", agent.ShortID(task.ID), method, err)
|
||||||
if err := task.Transition(StatusResolving); err == nil {
|
if err := task.Transition(StatusResolving); err == nil {
|
||||||
m.processTaskRetry(ctx, task)
|
m.processTaskRetry(ctx, task)
|
||||||
|
|
@ -432,7 +437,7 @@ func (m *Manager) processTask(ctx context.Context, task *Task) {
|
||||||
|
|
||||||
// processTaskRetry handles fallback after a method failure.
|
// processTaskRetry handles fallback after a method failure.
|
||||||
func (m *Manager) processTaskRetry(ctx context.Context, task *Task) {
|
func (m *Manager) processTaskRetry(ctx context.Context, task *Task) {
|
||||||
method, err := resolveMethod(ctx, task, m.downloaders)
|
method, err := resolveMethod(ctx, task, m.downloaders, m.cfg.PreferredMethods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.fail(ctx, task, "fallback failed: "+err.Error())
|
m.fail(ctx, task, "fallback failed: "+err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,47 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resolveMethod determines which download method to use for a task.
|
// effectiveOrder returns the ordered methods to try for a task.
|
||||||
// For "auto": tries available methods in priority order (torrent > debrid > usenet).
|
//
|
||||||
// For specific method: uses only that method.
|
// The agent's local config (configMethods, from config.toml `preferred_methods`)
|
||||||
func resolveMethod(ctx context.Context, task *Task, downloaders map[DownloadMethod]Downloader) (DownloadMethod, error) {
|
// WINS and gates: only the listed methods are eligible, in that order — so a
|
||||||
var order []DownloadMethod
|
// "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 {
|
switch task.PreferredMethod {
|
||||||
case "torrent":
|
case "torrent":
|
||||||
order = []DownloadMethod{MethodTorrent}
|
return []DownloadMethod{MethodTorrent}
|
||||||
case "debrid":
|
case "debrid":
|
||||||
order = []DownloadMethod{MethodDebrid}
|
return []DownloadMethod{MethodDebrid}
|
||||||
case "usenet":
|
case "usenet":
|
||||||
order = []DownloadMethod{MethodUsenet}
|
return []DownloadMethod{MethodUsenet}
|
||||||
default: // "auto"
|
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 {
|
for _, method := range order {
|
||||||
// Skip already-tried methods
|
// 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.
|
// tryFallback attempts to fall back to the next untried download method WITHIN
|
||||||
// Returns true if fallback was initiated, false if no more methods.
|
// the effective order. A single-method order (e.g. "debrid only") has no
|
||||||
func tryFallback(task *Task, downloaders map[DownloadMethod]Downloader) bool {
|
// fallback — failing over to torrent would defeat the whole preference.
|
||||||
if task.PreferredMethod != "auto" {
|
func tryFallback(task *Task, downloaders map[DownloadMethod]Downloader, configMethods []string) bool {
|
||||||
return false // specific method requested, no fallback
|
order := effectiveOrder(task, configMethods)
|
||||||
|
if len(order) <= 1 {
|
||||||
|
return false // single method requested, no fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
task.TriedMethods = append(task.TriedMethods, task.ResolvedMethod)
|
task.TriedMethods = append(task.TriedMethods, task.ResolvedMethod)
|
||||||
|
|
||||||
available := make([]DownloadMethod, 0, len(downloaders))
|
for _, m := range order {
|
||||||
for m := range downloaders {
|
tried := false
|
||||||
available = append(available, m)
|
for _, tm := range task.TriedMethods {
|
||||||
|
if tm == m {
|
||||||
|
tried = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tried {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := downloaders[m]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
return task.HasUntried(available)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ func TestResolveMethodAuto(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := &Task{PreferredMethod: "auto"}
|
task := &Task{PreferredMethod: "auto"}
|
||||||
method, err := resolveMethod(context.Background(), task, downloaders)
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ func TestResolveMethodSpecific(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := &Task{PreferredMethod: "debrid"}
|
task := &Task{PreferredMethod: "debrid"}
|
||||||
method, err := resolveMethod(context.Background(), task, downloaders)
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ func TestResolveMethodSkipsTried(t *testing.T) {
|
||||||
PreferredMethod: "auto",
|
PreferredMethod: "auto",
|
||||||
TriedMethods: []DownloadMethod{MethodTorrent},
|
TriedMethods: []DownloadMethod{MethodTorrent},
|
||||||
}
|
}
|
||||||
method, err := resolveMethod(context.Background(), task, downloaders)
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +82,7 @@ func TestResolveMethodNoneAvailable(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := &Task{PreferredMethod: "auto"}
|
task := &Task{PreferredMethod: "auto"}
|
||||||
_, err := resolveMethod(context.Background(), task, downloaders)
|
_, err := resolveMethod(context.Background(), task, downloaders, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error when no method available")
|
t.Error("expected error when no method available")
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +95,7 @@ func TestResolveMethodAvailabilityError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := &Task{ID: "test-resolve-err", PreferredMethod: "auto"}
|
task := &Task{ID: "test-resolve-err", PreferredMethod: "auto"}
|
||||||
method, err := resolveMethod(context.Background(), task, downloaders)
|
method, err := resolveMethod(context.Background(), task, downloaders, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +116,7 @@ func TestTryFallbackAutoMode(t *testing.T) {
|
||||||
ResolvedMethod: MethodTorrent,
|
ResolvedMethod: MethodTorrent,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tryFallback(task, downloaders) {
|
if !tryFallback(task, downloaders, nil) {
|
||||||
t.Error("should have fallback available")
|
t.Error("should have fallback available")
|
||||||
}
|
}
|
||||||
if len(task.TriedMethods) != 1 || task.TriedMethods[0] != MethodTorrent {
|
if len(task.TriedMethods) != 1 || task.TriedMethods[0] != MethodTorrent {
|
||||||
|
|
@ -135,7 +135,82 @@ func TestTryFallbackSpecificMode(t *testing.T) {
|
||||||
ResolvedMethod: MethodTorrent,
|
ResolvedMethod: MethodTorrent,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tryFallback(task, downloaders) {
|
if tryFallback(task, downloaders, nil) {
|
||||||
t.Error("should not fallback in specific mode")
|
t.Error("should not fallback in specific mode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── config.toml preferred_methods gating ──────────────────────────────────
|
||||||
|
|
||||||
|
// Config list wins over the per-task preference: a "debrid only" agent picks
|
||||||
|
// debrid even when the task says auto AND torrent is available (the old bug:
|
||||||
|
// torrent-first auto stole every download from debrid-only users).
|
||||||
|
func TestResolveMethodConfigListWins(t *testing.T) {
|
||||||
|
downloaders := map[DownloadMethod]Downloader{
|
||||||
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
||||||
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
||||||
|
}
|
||||||
|
task := &Task{PreferredMethod: "auto"} // web said auto
|
||||||
|
method, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if method != MethodDebrid {
|
||||||
|
t.Errorf("method = %q, want debrid (config list debrid-only wins)", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A method not in the config list is NEVER used, even if it's the only one
|
||||||
|
// available — failing closed is the whole point of "debrid only".
|
||||||
|
func TestResolveMethodConfigListGatesOutTorrent(t *testing.T) {
|
||||||
|
downloaders := map[DownloadMethod]Downloader{
|
||||||
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
||||||
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: false},
|
||||||
|
}
|
||||||
|
task := &Task{PreferredMethod: "auto"}
|
||||||
|
_, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid"})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error: torrent is available but not in the debrid-only list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered list honours order: debrid first, then usenet.
|
||||||
|
func TestResolveMethodConfigListOrder(t *testing.T) {
|
||||||
|
downloaders := map[DownloadMethod]Downloader{
|
||||||
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
||||||
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: false},
|
||||||
|
MethodUsenet: &mockDownloader{method: MethodUsenet, available: true},
|
||||||
|
}
|
||||||
|
task := &Task{PreferredMethod: "auto"}
|
||||||
|
method, err := resolveMethod(context.Background(), task, downloaders, []string{"debrid", "usenet"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if method != MethodUsenet {
|
||||||
|
t.Errorf("method = %q, want usenet (debrid unavailable, next in list)", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A multi-method config list allows fallback within the list...
|
||||||
|
func TestTryFallbackConfigListMulti(t *testing.T) {
|
||||||
|
downloaders := map[DownloadMethod]Downloader{
|
||||||
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
||||||
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
||||||
|
}
|
||||||
|
task := &Task{PreferredMethod: "auto", ResolvedMethod: MethodDebrid}
|
||||||
|
if !tryFallback(task, downloaders, []string{"debrid", "torrent"}) {
|
||||||
|
t.Error("should fall back to torrent (in the list)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...but a single-method config list has no fallback (debrid-only never torrents).
|
||||||
|
func TestTryFallbackConfigListSingle(t *testing.T) {
|
||||||
|
downloaders := map[DownloadMethod]Downloader{
|
||||||
|
MethodTorrent: &mockDownloader{method: MethodTorrent, available: true},
|
||||||
|
MethodDebrid: &mockDownloader{method: MethodDebrid, available: true},
|
||||||
|
}
|
||||||
|
task := &Task{PreferredMethod: "auto", ResolvedMethod: MethodDebrid}
|
||||||
|
if tryFallback(task, downloaders, []string{"debrid"}) {
|
||||||
|
t.Error("debrid-only must not fall back to torrent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue