Repository: AlistGo/alist (language: go)

Issue:
是否可以添加115离线下载接口
### Please make sure of the following things

- [X] I have read the [documentation](https://alist.nn.ci).
- [X] I'm sure there are no duplicate issues or discussions.
- [ ] I'm sure this feature is not implemented.
- [X] I'm sure it's a reasonable and popular requirement.

### Description of the feature / 需求描述

既然适配了pikpak，可不可以适配一下115的云下载呢，115是国内网盘，使用的人也很多，可不可以调用一下云下载的功能

### Suggested solution / 实现思路

_No response_

### Additional context / 附件

_No response_


Discussion / hints:
### See
1. 53% #880

<!-- Created by actions-cool/issues-similarity-analysis. Do not remove. -->
> ### See
> 1. 53% [希望可以添加离线下载功能 #880](https://github.com/alist-org/alist/issues/880)

不一样，我是要115的云下载接入
参考：https://greasyfork.org/zh-CN/scripts/484774-115%E7%A6%BB%E7%BA%BF%E4%B8%8B%E8%BD%BD%E5%B7%A5%E5%85%B7-%E8%87%AA%E7%94%A8-%E6%89%8B%E5%8A%A8%E5%A1%AB%E5%86%99cookie/code

或者参考这个：https://github.com/coolzilj/lixian-115/blob/master/index.js
alist本来也是通过cookie挂载的，把调用的部分嵌到离线下载的选项中这样
可以考虑，待我研究一下

Fix patch:
diff --git a/drivers/115/driver.go b/drivers/115/driver.go
index 57b6c45f9e7..2a1c8deef47 100644
--- a/drivers/115/driver.go
+++ b/drivers/115/driver.go
@@ -63,7 +63,7 @@ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
 	if err := d.WaitLimit(ctx); err != nil {
 		return nil, err
 	}
-	var userAgent = args.Header.Get("User-Agent")
+	userAgent := args.Header.Get("User-Agent")
 	downloadInfo, err := d.
 		DownloadWithUA(file.(*FileObj).PickCode, userAgent)
 	if err != nil {
@@ -179,7 +179,22 @@ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
 	}
 	// 分片上传
 	return d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID)
+}
+
+func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
+	resp, err := d.client.ListOfflineTask(0)
+	if err != nil {
+		return nil, err
+	}
+	return resp.Tasks, nil
+}
+
+func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
+	return d.client.AddOfflineTaskURIs(uris, dstDir.GetID())
+}
 
+func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
+	return d.client.DeleteOfflineTasks(hashes, deleteFiles)
 }
 
 var _ driver.Driver = (*Pan115)(nil)
diff --git a/go.mod b/go.mod
index 8bed00741b5..8a1eada4b92 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/alist-org/alist/v3
 go 1.22.4
 
 require (
-	github.com/SheltonZhu/115driver v1.0.25
+	github.com/SheltonZhu/115driver v1.0.26
 	github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
 	github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
 	github.com/alist-org/gofakes3 v0.0.7
diff --git a/go.sum b/go.sum
index d82af70ecf2..01929328eea 100644
--- a/go.sum
+++ b/go.sum
@@ -7,8 +7,8 @@ github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9
 github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
 github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
 github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
-github.com/SheltonZhu/115driver v1.0.25 h1:i101yanLKUwV1Pi7x+vgNOwgz7Hp9JbNmo6BCZ9/4wo=
-github.com/SheltonZhu/115driver v1.0.25/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
+github.com/SheltonZhu/115driver v1.0.26 h1:UDUEZffJoQLFYs2nxnyxqvxwSaocxP4LNaOycVY6syU=
+github.com/SheltonZhu/115driver v1.0.26/go.mod h1:e3fPOBANbH/FsTya8FquJwOR3ErhCQgEab3q6CVY2k4=
 github.com/Unknwon/goconfig v1.0.0 h1:9IAu/BYbSLQi8puFjUQApZTxIHqSwrj5d8vpP8vTq4A=
 github.com/Unknwon/goconfig v1.0.0/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
 github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
@@ -548,8 +548,6 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
 golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
-golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
-golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
diff --git a/internal/offline_download/115/client.go b/internal/offline_download/115/client.go
new file mode 100644
index 00000000000..0ebf38ffced
--- /dev/null
+++ b/internal/offline_download/115/client.go
@@ -0,0 +1,124 @@
+package _115
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/alist-org/alist/v3/drivers/115"
+	"github.com/alist-org/alist/v3/internal/errs"
+	"github.com/alist-org/alist/v3/internal/model"
+	"github.com/alist-org/alist/v3/internal/offline_download/tool"
+	"github.com/alist-org/alist/v3/internal/op"
+)
+
+type Cloud115 struct {
+	refreshTaskCache bool
+}
+
+func (p *Cloud115) Name() string {
+	return "115 Cloud"
+}
+
+func (p *Cloud115) Items() []model.SettingItem {
+	return nil
+}
+
+func (p *Cloud115) Run(task *tool.DownloadTask) error {
+	return errs.NotSupport
+}
+
+func (p *Cloud115) Init() (string, error) {
+	p.refreshTaskCache = false
+	return "ok", nil
+}
+
+func (p *Cloud115) IsReady() bool {
+	return true
+}
+
+func (p *Cloud115) AddURL(args *tool.AddUrlArgs) (string, error) {
+	// 添加新任务刷新缓存
+	p.refreshTaskCache = true
+	// args.TempDir 已经被修改为了 DstDirPath
+	storage, actualPath, err := op.GetStorageAndActualPath(args.TempDir)
+	if err != nil {
+		return "", err
+	}
+	driver115, ok := storage.(*_115.Pan115)
+	if !ok {
+		return "", fmt.Errorf("unsupported storage driver for offline download, only 115 Cloud is supported")
+	}
+
+	ctx := context.Background()
+	parentDir, err := op.GetUnwrap(ctx, storage, actualPath)
+	if err != nil {
+		return "", err
+	}
+
+	hashs, err := driver115.OfflineDownload(ctx, []string{args.Url}, parentDir)
+	if err != nil || len(hashs) < 1 {
+		return "", fmt.Errorf("failed to add offline download task: %w", err)
+	}
+
+	return hashs[0], nil
+}
+
+func (p *Cloud115) Remove(task *tool.DownloadTask) error {
+	storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
+	if err != nil {
+		return err
+	}
+	driver115, ok := storage.(*_115.Pan115)
+	if !ok {
+		return fmt.Errorf("unsupported storage driver for offline download, only 115 Cloud is supported")
+	}
+
+	ctx := context.Background()
+	if err := driver115.DeleteOfflineTasks(ctx, []string{task.GID}, false); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (p *Cloud115) Status(task *tool.DownloadTask) (*tool.Status, error) {
+	storage, _, err := op.GetStorageAndActualPath(task.DstDirPath)
+	if err != nil {
+		return nil, err
+	}
+	driver115, ok := storage.(*_115.Pan115)
+	if !ok {
+		return nil, fmt.Errorf("unsupported storage driver for offline download, only 115 Cloud is supported")
+	}
+
+	tasks, err := driver115.OfflineList(context.Background())
+	if err != nil {
+		return nil, err
+	}
+
+	s := &tool.Status{
+		Progress:  0,
+		NewGID:    "",
+		Completed: false,
+		Status:    "the task has been deleted",
+		Err:       nil,
+	}
+	for _, t := range tasks {
+		if t.InfoHash == task.GID {
+			s.Progress = t.Percent
+			s.Status = t.GetStatus()
+			s.Completed = t.IsDone()
+			if t.IsFailed() {
+				s.Err = fmt.Errorf(t.GetStatus())
+			}
+			return s, nil
+		}
+	}
+	s.Err = fmt.Errorf("the task has been deleted")
+	return nil, nil
+}
+
+var _ tool.Tool = (*Cloud115)(nil)
+
+func init() {
+	tool.Tools.Add(&Cloud115{})
+}
diff --git a/internal/offline_download/all.go b/internal/offline_download/all.go
index 67869dee8d2..ee80b5a0b8f 100644
--- a/internal/offline_download/all.go
+++ b/internal/offline_download/all.go
@@ -1,6 +1,7 @@
 package offline_download
 
 import (
+	_ "github.com/alist-org/alist/v3/internal/offline_download/115"
 	_ "github.com/alist-org/alist/v3/internal/offline_download/aria2"
 	_ "github.com/alist-org/alist/v3/internal/offline_download/http"
 	_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go
index e9bcdc50e52..1cb314bab71 100644
--- a/internal/offline_download/tool/add.go
+++ b/internal/offline_download/tool/add.go
@@ -66,11 +66,18 @@ func AddURL(ctx context.Context, args *AddURLArgs) (tache.TaskWithInfo, error) {
 	uid := uuid.NewString()
 	tempDir := filepath.Join(conf.Conf.TempDir, args.Tool, uid)
 	deletePolicy := args.DeletePolicy
-	if args.Tool == "pikpak" {
+
+	switch args.Tool {
+	case "115 Cloud":
+		tempDir = args.DstDirPath
+		// 防止将下载好的文件删除
+		deletePolicy = DeleteNever
+	case "pikpak":
 		tempDir = args.DstDirPath
 		// 防止将下载好的文件删除
 		deletePolicy = DeleteNever
 	}
+	
 	t := &DownloadTask{
 		Url:          args.URL,
 		DstDirPath:   args.DstDirPath,
diff --git a/internal/offline_download/tool/download.go b/internal/offline_download/tool/download.go
index 79a29ef0c9a..2cb431964e9 100644
--- a/internal/offline_download/tool/download.go
+++ b/internal/offline_download/tool/download.go
@@ -47,9 +47,7 @@ func (t *DownloadTask) Run() error {
 		return err
 	}
 	t.GID = gid
-	var (
-		ok bool
-	)
+	var ok bool
 outer:
 	for {
 		select {
@@ -74,6 +72,15 @@ outer:
 	if t.tool.Name() == "pikpak" {
 		return nil
 	}
+	if t.tool.Name() == "115 Cloud" {
+		// hack for 115
+		<-time.After(time.Second * 1)
+		err := t.tool.Remove(t)
+		if err != nil {
+			log.Errorln(err.Error())
+		}
+		return nil
+	}
 	t.Status = "offline download completed, maybe transferring"
 	// hack for qBittorrent
 	if t.tool.Name() == "qBittorrent" {
@@ -129,6 +136,9 @@ func (t *DownloadTask) Complete() error {
 	if t.tool.Name() == "pikpak" {
 		return nil
 	}
+	if t.tool.Name() == "115 Cloud" {
+		return nil
+	}
 	if getFileser, ok := t.tool.(GetFileser); ok {
 		files = getFileser.GetFiles(t)
 	} else {
@@ -158,6 +168,4 @@ func (t *DownloadTask) GetStatus() string {
 	return t.Status
 }
 
-var (
-	DownloadTaskManager *tache.Manager[*DownloadTask]
-)
+var DownloadTaskManager *tache.Manager[*DownloadTask]


Test patch: