From 666637896db401e42c8f3836853d270d3c4f0e00 Mon Sep 17 00:00:00 2001 From: AmPlace <81105770+AmPlace@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:16:39 +0800 Subject: [PATCH 1/3] fix(offline): allow ED2K downloads through 115 tools - Treat 115 Cloud and 115 Open as ED2K-capable tools. - Keep automatic fallback limited to Thunder tools. - Add regression coverage for supported and unsupported tools. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- internal/offline_download/tool/add.go | 9 +++++--- internal/offline_download/tool/add_test.go | 26 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 internal/offline_download/tool/add_test.go diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index e147d159e..3c70657c1 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -217,8 +217,11 @@ func isEd2kURL(urlStr string) bool { return strings.HasPrefix(strings.ToLower(urlStr), "ed2k://") } -// ed2kCapableTools 支持 ed2k 协议的工具列表(迅雷系) -var ed2kCapableTools = []string{"Thunder", "ThunderX", "ThunderBrowser"} +// ed2kCapableTools 支持 ed2k 协议的工具列表 +var ed2kCapableTools = []string{"115 Cloud", "115 Open", "Thunder", "ThunderX", "ThunderBrowser"} + +// ed2kFallbackTools 是当前可用于自动接管 ed2k 任务的工具列表。 +var ed2kFallbackTools = []string{"Thunder", "ThunderX", "ThunderBrowser"} // isEd2kCapableTool 检查工具是否支持 ed2k 协议 func isEd2kCapableTool(toolName string) bool { @@ -232,7 +235,7 @@ func isEd2kCapableTool(toolName string) bool { // findEd2kCapableTool 查找一个可用的支持 ed2k 的工具 func findEd2kCapableTool() (Tool, string) { - for _, name := range ed2kCapableTools { + for _, name := range ed2kFallbackTools { t, err := Tools.Get(name) if err != nil { continue diff --git a/internal/offline_download/tool/add_test.go b/internal/offline_download/tool/add_test.go new file mode 100644 index 000000000..93399a810 --- /dev/null +++ b/internal/offline_download/tool/add_test.go @@ -0,0 +1,26 @@ +package tool + +import "testing" + +func TestIsEd2kCapableTool(t *testing.T) { + tests := []struct { + name string + want bool + }{ + {name: "115 Cloud", want: true}, + {name: "115 Open", want: true}, + {name: "Thunder", want: true}, + {name: "ThunderX", want: true}, + {name: "ThunderBrowser", want: true}, + {name: "aria2", want: false}, + {name: "SimpleHttp", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isEd2kCapableTool(tt.name); got != tt.want { + t.Fatalf("isEd2kCapableTool(%q) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} From 921ce27d12dcad744d67d0919a12cc5de2f032ae Mon Sep 17 00:00:00 2001 From: AmPlace <81105770+AmPlace@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:42:15 +0800 Subject: [PATCH 2/3] fix(offline): route ED2K to native 115 tools - Allow ED2K requests initially using SimpleHttp to enter tool routing. - Prefer the matching 115 tool for 115 Cloud and 115 Open destinations. - Add regression coverage for native storage tool selection. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- internal/offline_download/tool/add.go | 30 +++++++++++++++++----- internal/offline_download/tool/add_test.go | 28 +++++++++++++++++++- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index 3c70657c1..52026ca2f 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -18,6 +18,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser" "github.com/OpenListTeam/OpenList/v4/drivers/thunderx" "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/fs" "github.com/OpenListTeam/OpenList/v4/internal/model" @@ -69,7 +70,7 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro } } // try putting url - if args.Tool == "SimpleHttp" { + if args.Tool == "SimpleHttp" && !isEd2kURL(args.URL) { if isSimpleHttpSchemeUnsupported(args.URL) { return nil, fmt.Errorf("SimpleHttp tool does not support this URL scheme, please use aria2 or other tools for magnet/ed2k links") } @@ -83,13 +84,17 @@ func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, erro // ed2k 链接自动路由:如果当前工具不支持 ed2k,自动尝试使用迅雷系工具 if isEd2kURL(args.URL) { if !isEd2kCapableTool(args.Tool) { - // 尝试找到一个可用的支持 ed2k 的工具 - fallbackTool, fallbackName := findEd2kCapableTool() - if fallbackTool != nil { - // 使用找到的迅雷工具替代 - args.Tool = fallbackName + if storageTool := ed2kToolForStorage(storage); storageTool != "" { + // Prefer the matching native tool when the destination storage supports ed2k. + args.Tool = storageTool } else { - return nil, fmt.Errorf("ed2k protocol is not supported by %s. Please configure and use Thunder/ThunderX/ThunderBrowser for ed2k links", args.Tool) + // Otherwise, try to find an available Thunder-family tool. + fallbackTool, fallbackName := findEd2kCapableTool() + if fallbackTool != nil { + args.Tool = fallbackName + } else { + return nil, fmt.Errorf("ed2k protocol is not supported by %s. Please configure and use Thunder/ThunderX/ThunderBrowser for ed2k links", args.Tool) + } } } } @@ -217,6 +222,17 @@ func isEd2kURL(urlStr string) bool { return strings.HasPrefix(strings.ToLower(urlStr), "ed2k://") } +func ed2kToolForStorage(storage driver.Driver) string { + switch storage.(type) { + case *_115.Pan115: + return "115 Cloud" + case *_115_open.Open115: + return "115 Open" + default: + return "" + } +} + // ed2kCapableTools 支持 ed2k 协议的工具列表 var ed2kCapableTools = []string{"115 Cloud", "115 Open", "Thunder", "ThunderX", "ThunderBrowser"} diff --git a/internal/offline_download/tool/add_test.go b/internal/offline_download/tool/add_test.go index 93399a810..3f53aded9 100644 --- a/internal/offline_download/tool/add_test.go +++ b/internal/offline_download/tool/add_test.go @@ -1,6 +1,12 @@ package tool -import "testing" +import ( + "testing" + + _115 "github.com/OpenListTeam/OpenList/v4/drivers/115" + _115_open "github.com/OpenListTeam/OpenList/v4/drivers/115_open" + "github.com/OpenListTeam/OpenList/v4/internal/driver" +) func TestIsEd2kCapableTool(t *testing.T) { tests := []struct { @@ -24,3 +30,23 @@ func TestIsEd2kCapableTool(t *testing.T) { }) } } + +func TestEd2kToolForStorage(t *testing.T) { + tests := []struct { + name string + storage driver.Driver + want string + }{ + {name: "115 Cloud", storage: &_115.Pan115{}, want: "115 Cloud"}, + {name: "115 Open", storage: &_115_open.Open115{}, want: "115 Open"}, + {name: "other", storage: nil, want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ed2kToolForStorage(tt.storage); got != tt.want { + t.Fatalf("ed2kToolForStorage(%T) = %q, want %q", tt.storage, got, tt.want) + } + }) + } +} From aff7c2ca3ace6fc1c7bc7aea6027bd7b3c1f28eb Mon Sep 17 00:00:00 2001 From: AmPlace <81105770+AmPlace@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:18:02 +0800 Subject: [PATCH 3/3] fix(offline): expose native tools for destination storage - Include the native destination tool in the path-aware tool list. - Keep existing ready-tool filtering for external destinations. - Add coverage for native storage to tool mapping. Co-authored-by: Codex <267193182+codex@users.noreply.github.com> --- internal/offline_download/tool/add.go | 23 +++++++++++++++ internal/offline_download/tool/add_test.go | 34 ++++++++++++++++++++++ internal/offline_download/tool/tools.go | 26 +++++++++++++++++ server/handles/offline_download.go | 2 +- 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index 52026ca2f..12160dcfe 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -223,11 +223,34 @@ func isEd2kURL(urlStr string) bool { } func ed2kToolForStorage(storage driver.Driver) string { + switch toolNameForStorage(storage) { + case "115 Cloud", "115 Open": + return toolNameForStorage(storage) + default: + return "" + } +} + +func toolNameForStorage(storage driver.Driver) string { switch storage.(type) { case *_115.Pan115: return "115 Cloud" case *_115_open.Open115: return "115 Open" + case *_123.Pan123: + return "123Pan" + case *_123_open.Open123: + return "123 Open" + case *guangyapan.GuangYaPan: + return "GuangYaPan" + case *pikpak.PikPak: + return "PikPak" + case *thunder.Thunder: + return "Thunder" + case *thunderx.ThunderX: + return "ThunderX" + case *thunder_browser.ThunderBrowser, *thunder_browser.ThunderBrowserExpert: + return "ThunderBrowser" default: return "" } diff --git a/internal/offline_download/tool/add_test.go b/internal/offline_download/tool/add_test.go index 3f53aded9..c2782130f 100644 --- a/internal/offline_download/tool/add_test.go +++ b/internal/offline_download/tool/add_test.go @@ -5,6 +5,13 @@ import ( _115 "github.com/OpenListTeam/OpenList/v4/drivers/115" _115_open "github.com/OpenListTeam/OpenList/v4/drivers/115_open" + _123 "github.com/OpenListTeam/OpenList/v4/drivers/123" + _123_open "github.com/OpenListTeam/OpenList/v4/drivers/123_open" + "github.com/OpenListTeam/OpenList/v4/drivers/guangyapan" + "github.com/OpenListTeam/OpenList/v4/drivers/pikpak" + "github.com/OpenListTeam/OpenList/v4/drivers/thunder" + "github.com/OpenListTeam/OpenList/v4/drivers/thunder_browser" + "github.com/OpenListTeam/OpenList/v4/drivers/thunderx" "github.com/OpenListTeam/OpenList/v4/internal/driver" ) @@ -50,3 +57,30 @@ func TestEd2kToolForStorage(t *testing.T) { }) } } + +func TestToolNameForStorage(t *testing.T) { + tests := []struct { + name string + storage driver.Driver + want string + }{ + {name: "115 Cloud", storage: &_115.Pan115{}, want: "115 Cloud"}, + {name: "115 Open", storage: &_115_open.Open115{}, want: "115 Open"}, + {name: "123Pan", storage: &_123.Pan123{}, want: "123Pan"}, + {name: "123 Open", storage: &_123_open.Open123{}, want: "123 Open"}, + {name: "GuangYaPan", storage: &guangyapan.GuangYaPan{}, want: "GuangYaPan"}, + {name: "PikPak", storage: &pikpak.PikPak{}, want: "PikPak"}, + {name: "Thunder", storage: &thunder.Thunder{}, want: "Thunder"}, + {name: "ThunderX", storage: &thunderx.ThunderX{}, want: "ThunderX"}, + {name: "ThunderBrowser", storage: &thunder_browser.ThunderBrowser{}, want: "ThunderBrowser"}, + {name: "other", storage: nil, want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toolNameForStorage(tt.storage); got != tt.want { + t.Fatalf("toolNameForStorage(%T) = %q, want %q", tt.storage, got, tt.want) + } + }) + } +} diff --git a/internal/offline_download/tool/tools.go b/internal/offline_download/tool/tools.go index d5a90f504..fa7fd2608 100644 --- a/internal/offline_download/tool/tools.go +++ b/internal/offline_download/tool/tools.go @@ -5,6 +5,7 @@ import ( "sort" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" ) var ( @@ -35,6 +36,31 @@ func (t ToolsManager) Names() []string { return names } +// NamesForPath returns ready tools and the native tool for the destination storage. +// Native tools can write directly to their own storage even without a temporary path setting. +func (t ToolsManager) NamesForPath(path string) []string { + names := t.Names() + storage, _, err := op.GetStorageAndActualPath(path) + if err != nil { + return names + } + + name := toolNameForStorage(storage) + if name == "" { + return names + } + for _, existing := range names { + if existing == name { + return names + } + } + if _, ok := t[name]; ok { + names = append(names, name) + sort.Strings(names) + } + return names +} + func (t ToolsManager) Items() []model.SettingItem { var items []model.SettingItem for _, tool := range t { diff --git a/server/handles/offline_download.go b/server/handles/offline_download.go index 5e2befee0..4d4167ba8 100644 --- a/server/handles/offline_download.go +++ b/server/handles/offline_download.go @@ -518,7 +518,7 @@ func SetGuangYaPan(c *gin.Context) { } func OfflineDownloadTools(c *gin.Context) { - tools := tool.Tools.Names() + tools := tool.Tools.NamesForPath(c.Query("path")) common.SuccessResp(c, tools) }