parent
6106a2d4cc
commit
bdf4b52885
3
go.mod
3
go.mod
@ -33,6 +33,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/hekmon/transmissionrpc/v3 v3.0.0
|
||||||
github.com/hirochachacha/go-smb2 v1.1.0
|
github.com/hirochachacha/go-smb2 v1.1.0
|
||||||
github.com/ipfs/go-ipfs-api v0.7.0
|
github.com/ipfs/go-ipfs-api v0.7.0
|
||||||
github.com/jlaffaye/ftp v0.2.0
|
github.com/jlaffaye/ftp v0.2.0
|
||||||
@ -82,6 +83,8 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hekmon/cunits/v2 v2.1.0 // indirect
|
||||||
github.com/ipfs/boxo v0.12.0 // indirect
|
github.com/ipfs/boxo v0.12.0 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
6
go.sum
6
go.sum
@ -240,11 +240,17 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI0=
|
||||||
|
github.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M=
|
||||||
|
github.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ=
|
||||||
|
github.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg=
|
||||||
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
|
||||||
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
@ -54,6 +54,10 @@ const (
|
|||||||
Aria2Uri = "aria2_uri"
|
Aria2Uri = "aria2_uri"
|
||||||
Aria2Secret = "aria2_secret"
|
Aria2Secret = "aria2_secret"
|
||||||
|
|
||||||
|
// transmission
|
||||||
|
TransmissionUri = "transmission_uri"
|
||||||
|
TransmissionSeedtime = "transmission_seedtime"
|
||||||
|
|
||||||
// single
|
// single
|
||||||
Token = "token"
|
Token = "token"
|
||||||
IndexProgress = "index_progress"
|
IndexProgress = "index_progress"
|
||||||
|
@ -6,4 +6,5 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
|
_ "github.com/alist-org/alist/v3/internal/offline_download/http"
|
||||||
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
|
_ "github.com/alist-org/alist/v3/internal/offline_download/pikpak"
|
||||||
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
|
_ "github.com/alist-org/alist/v3/internal/offline_download/qbit"
|
||||||
|
_ "github.com/alist-org/alist/v3/internal/offline_download/transmission"
|
||||||
)
|
)
|
||||||
|
@ -101,6 +101,19 @@ outer:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.tool.Name() == "transmission" {
|
||||||
|
// hack for transmission
|
||||||
|
seedTime := setting.GetInt(conf.TransmissionSeedtime, 0)
|
||||||
|
if seedTime >= 0 {
|
||||||
|
t.Status = "offline download completed, waiting for seeding"
|
||||||
|
<-time.After(time.Minute * time.Duration(seedTime))
|
||||||
|
err := t.tool.Remove(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
176
internal/offline_download/transmission/client.go
Normal file
176
internal/offline_download/transmission/client.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package transmission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"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/setting"
|
||||||
|
"github.com/hekmon/transmissionrpc/v3"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transmission struct {
|
||||||
|
client *transmissionrpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Run(task *tool.DownloadTask) error {
|
||||||
|
return errs.NotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Name() string {
|
||||||
|
return "transmission"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Items() []model.SettingItem {
|
||||||
|
// transmission settings
|
||||||
|
return []model.SettingItem{
|
||||||
|
{Key: conf.TransmissionUri, Value: "http://localhost:9091/transmission/rpc", Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
|
||||||
|
{Key: conf.TransmissionSeedtime, Value: "0", Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Init() (string, error) {
|
||||||
|
t.client = nil
|
||||||
|
uri := setting.GetStr(conf.TransmissionUri)
|
||||||
|
endpoint, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to init transmission client")
|
||||||
|
}
|
||||||
|
c, err := transmissionrpc.New(endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to init transmission client")
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, serverVersion, serverMinimumVersion, err := c.RPCVersion(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrapf(err, "failed get transmission version")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("remote transmission RPC version (v%d) is incompatible with the transmission library (v%d): remote needs at least v%d",
|
||||||
|
serverVersion, transmissionrpc.RPCVersion, serverMinimumVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.client = c
|
||||||
|
log.Infof("remote transmission RPC version (v%d) is compatible with our transmissionrpc library (v%d)\n",
|
||||||
|
serverVersion, transmissionrpc.RPCVersion)
|
||||||
|
log.Infof("using transmission version: %d", serverVersion)
|
||||||
|
return fmt.Sprintf("transmission version: %d", serverVersion), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) IsReady() bool {
|
||||||
|
return t.client != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) {
|
||||||
|
endpoint, err := url.Parse(args.Url)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to parse transmission uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcPayload := transmissionrpc.TorrentAddPayload{
|
||||||
|
DownloadDir: &args.TempDir,
|
||||||
|
}
|
||||||
|
// http url for .torrent file
|
||||||
|
if endpoint.Scheme == "http" || endpoint.Scheme == "https" {
|
||||||
|
resp, err := http.Get(args.Url)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to get .torrent file")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := base64.NewEncoder(base64.StdEncoding, buffer)
|
||||||
|
// Stream file to the encoder
|
||||||
|
if _, err = io.Copy(encoder, resp.Body); err != nil {
|
||||||
|
return "", errors.Wrap(err, "can't copy file content into the base64 encoder")
|
||||||
|
}
|
||||||
|
// Flush last bytes
|
||||||
|
if err = encoder.Close(); err != nil {
|
||||||
|
return "", errors.Wrap(err, "can't flush last bytes of the base64 encoder")
|
||||||
|
}
|
||||||
|
// Get the string form
|
||||||
|
b64 := buffer.String()
|
||||||
|
rpcPayload.MetaInfo = &b64
|
||||||
|
} else { // magnet uri
|
||||||
|
rpcPayload.Filename = &args.Url
|
||||||
|
}
|
||||||
|
|
||||||
|
torrent, err := t.client.TorrentAdd(context.TODO(), rpcPayload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if torrent.ID == nil {
|
||||||
|
return "", fmt.Errorf("failed get torrent ID")
|
||||||
|
}
|
||||||
|
gid := strconv.FormatInt(*torrent.ID, 10)
|
||||||
|
return gid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Remove(task *tool.DownloadTask) error {
|
||||||
|
gid, err := strconv.ParseInt(task.GID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = t.client.TorrentRemove(context.TODO(), transmissionrpc.TorrentRemovePayload{
|
||||||
|
IDs: []int64{gid},
|
||||||
|
DeleteLocalData: false,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transmission) Status(task *tool.DownloadTask) (*tool.Status, error) {
|
||||||
|
gid, err := strconv.ParseInt(task.GID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
infos, err := t.client.TorrentGetAllFor(context.TODO(), []int64{gid})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(infos) < 1 {
|
||||||
|
return nil, fmt.Errorf("failed get status, wrong gid: %s", task.GID)
|
||||||
|
}
|
||||||
|
info := infos[0]
|
||||||
|
|
||||||
|
s := &tool.Status{
|
||||||
|
Completed: *info.IsFinished,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
s.Progress = *info.PercentDone * 100
|
||||||
|
|
||||||
|
switch *info.Status {
|
||||||
|
case transmissionrpc.TorrentStatusCheckWait,
|
||||||
|
transmissionrpc.TorrentStatusDownloadWait,
|
||||||
|
transmissionrpc.TorrentStatusCheck,
|
||||||
|
transmissionrpc.TorrentStatusDownload,
|
||||||
|
transmissionrpc.TorrentStatusIsolated:
|
||||||
|
s.Status = "[transmission] " + info.Status.String()
|
||||||
|
case transmissionrpc.TorrentStatusSeedWait,
|
||||||
|
transmissionrpc.TorrentStatusSeed:
|
||||||
|
s.Completed = true
|
||||||
|
case transmissionrpc.TorrentStatusStopped:
|
||||||
|
s.Err = errors.Errorf("[transmission] failed to download %s, status: %s, error: %s", task.GID, info.Status.String(), *info.ErrorString)
|
||||||
|
default:
|
||||||
|
s.Err = errors.Errorf("[transmission] unknown status occurred downloading %s, err: %s", task.GID, *info.ErrorString)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ tool.Tool = (*Transmission)(nil)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tool.Tools.Add(&Transmission{})
|
||||||
|
}
|
@ -30,6 +30,10 @@ func SetAria2(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_tool, err := tool.Tools.Get("aria2")
|
_tool, err := tool.Tools.Get("aria2")
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
version, err := _tool.Init()
|
version, err := _tool.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
@ -74,6 +78,37 @@ func OfflineDownloadTools(c *gin.Context) {
|
|||||||
common.SuccessResp(c, tools)
|
common.SuccessResp(c, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SetTransmissionReq struct {
|
||||||
|
Uri string `json:"uri" form:"uri"`
|
||||||
|
Seedtime string `json:"seedtime" form:"seedtime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTransmission(c *gin.Context) {
|
||||||
|
var req SetTransmissionReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items := []model.SettingItem{
|
||||||
|
{Key: conf.TransmissionUri, Value: req.Uri, Type: conf.TypeString, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
|
||||||
|
{Key: conf.TransmissionSeedtime, Value: req.Seedtime, Type: conf.TypeNumber, Group: model.OFFLINE_DOWNLOAD, Flag: model.PRIVATE},
|
||||||
|
}
|
||||||
|
if err := op.SaveSettingItems(items); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_tool, err := tool.Tools.Get("transmission")
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := _tool.Init(); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.SuccessResp(c, "ok")
|
||||||
|
}
|
||||||
|
|
||||||
type AddOfflineDownloadReq struct {
|
type AddOfflineDownloadReq struct {
|
||||||
Urls []string `json:"urls"`
|
Urls []string `json:"urls"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
@ -125,6 +125,7 @@ func admin(g *gin.RouterGroup) {
|
|||||||
setting.POST("/reset_token", handles.ResetToken)
|
setting.POST("/reset_token", handles.ResetToken)
|
||||||
setting.POST("/set_aria2", handles.SetAria2)
|
setting.POST("/set_aria2", handles.SetAria2)
|
||||||
setting.POST("/set_qbit", handles.SetQbittorrent)
|
setting.POST("/set_qbit", handles.SetQbittorrent)
|
||||||
|
setting.POST("/set_transmission", handles.SetTransmission)
|
||||||
|
|
||||||
task := g.Group("/task")
|
task := g.Group("/task")
|
||||||
handles.SetupTaskRoute(task)
|
handles.SetupTaskRoute(task)
|
||||||
@ -161,6 +162,7 @@ func _fs(g *gin.RouterGroup) {
|
|||||||
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
||||||
// g.POST("/add_aria2", handles.AddOfflineDownload)
|
// g.POST("/add_aria2", handles.AddOfflineDownload)
|
||||||
// g.POST("/add_qbit", handles.AddQbittorrent)
|
// g.POST("/add_qbit", handles.AddQbittorrent)
|
||||||
|
// g.POST("/add_transmission", handles.SetTransmission)
|
||||||
g.POST("/add_offline_download", handles.AddOfflineDownload)
|
g.POST("/add_offline_download", handles.AddOfflineDownload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user