Skip to content

Commit

Permalink
feat(offline_download): add transmission (close AlistGo#4102)
Browse files Browse the repository at this point in the history
  • Loading branch information
SheltonZhu committed Aug 16, 2024
1 parent 51c95ee commit af3988f
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 10 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
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/ipfs/go-ipfs-api v0.7.0
github.com/jlaffaye/ftp v0.2.0
Expand Down Expand Up @@ -84,6 +85,8 @@ require (
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // 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/jackc/puddle/v2 v2.2.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
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/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/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
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/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down Expand Up @@ -551,8 +557,6 @@ golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn5
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
Expand Down
12 changes: 8 additions & 4 deletions internal/conf/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,15 @@ const (
Aria2Uri = "aria2_uri"
Aria2Secret = "aria2_secret"

// transmission
TransmissionUri = "transmission_uri"
TransmissionSeedtime = "transmission_seedtime"

// single
Token = "token"
IndexProgress = "index_progress"

//SSO
// SSO
SSOClientId = "sso_client_id"
SSOClientSecret = "sso_client_secret"
SSOLoginEnabled = "sso_login_enabled"
Expand All @@ -73,7 +77,7 @@ const (
SSODefaultPermission = "sso_default_permission"
SSOCompatibilityMode = "sso_compatibility_mode"

//ldap
// ldap
LdapLoginEnabled = "ldap_login_enabled"
LdapServer = "ldap_server"
LdapManagerDN = "ldap_manager_dn"
Expand All @@ -84,7 +88,7 @@ const (
LdapDefaultDir = "ldap_default_dir"
LdapLoginTips = "ldap_login_tips"

//s3
// s3
S3Buckets = "s3_buckets"
S3AccessKeyId = "s3_access_key_id"
S3SecretAccessKey = "s3_secret_access_key"
Expand All @@ -97,7 +101,7 @@ const (
const (
UNKNOWN = iota
FOLDER
//OFFICE
// OFFICE
VIDEO
AUDIO
TEXT
Expand Down
1 change: 1 addition & 0 deletions internal/offline_download/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import (
_ "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/qbit"
_ "github.com/alist-org/alist/v3/internal/offline_download/transmission"
)
13 changes: 13 additions & 0 deletions internal/offline_download/tool/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
176 changes: 176 additions & 0 deletions internal/offline_download/transmission/client.go
Original file line number Diff line number Diff line change
@@ -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:
s.Status = "transmission: " + info.Status.String()
case transmissionrpc.TorrentStatusCheck,
transmissionrpc.TorrentStatusDownload,
transmissionrpc.TorrentStatusIsolated,
transmissionrpc.TorrentStatusStopped:
s.Status = "transmission: " + info.Status.String()
case transmissionrpc.TorrentStatusSeedWait,
transmissionrpc.TorrentStatusSeed:
s.Completed = true
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{})
}
35 changes: 35 additions & 0 deletions server/handles/offline_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func SetAria2(c *gin.Context) {
return
}
_tool, err := tool.Tools.Get("aria2")
if err != nil {
common.ErrorResp(c, err, 500)
return
}
version, err := _tool.Init()
if err != nil {
common.ErrorResp(c, err, 500)
Expand Down Expand Up @@ -74,6 +78,37 @@ func OfflineDownloadTools(c *gin.Context) {
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 {
Urls []string `json:"urls"`
Path string `json:"path"`
Expand Down
10 changes: 6 additions & 4 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func Init(e *gin.Engine) {
api.GET("/auth/get_sso_id", handles.SSOLoginCallback)
api.GET("/auth/sso_get_token", handles.SSOLoginCallback)

//webauthn
// webauthn
webauthn.GET("/webauthn_begin_registration", handles.BeginAuthnRegistration)
webauthn.POST("/webauthn_finish_registration", handles.FinishAuthnRegistration)
webauthn.GET("/webauthn_begin_login", handles.BeginAuthnLogin)
Expand Down Expand Up @@ -125,6 +125,7 @@ func admin(g *gin.RouterGroup) {
setting.POST("/reset_token", handles.ResetToken)
setting.POST("/set_aria2", handles.SetAria2)
setting.POST("/set_qbit", handles.SetQbittorrent)
setting.POST("/set_transmission", handles.SetTransmission)

task := g.Group("/task")
handles.SetupTaskRoute(task)
Expand Down Expand Up @@ -159,14 +160,15 @@ func _fs(g *gin.RouterGroup) {
g.PUT("/put", middlewares.FsUp, handles.FsStream)
g.PUT("/form", middlewares.FsUp, handles.FsForm)
g.POST("/link", middlewares.AuthAdmin, handles.Link)
//g.POST("/add_aria2", handles.AddOfflineDownload)
//g.POST("/add_qbit", handles.AddQbittorrent)
// g.POST("/add_aria2", handles.AddOfflineDownload)
// g.POST("/add_qbit", handles.AddQbittorrent)
// g.POST("/add_transmission", handles.SetTransmission)
g.POST("/add_offline_download", handles.AddOfflineDownload)
}

func Cors(r *gin.Engine) {
config := cors.DefaultConfig()
//config.AllowAllOrigins = true
// config.AllowAllOrigins = true
config.AllowOrigins = conf.Conf.Cors.AllowOrigins
config.AllowHeaders = conf.Conf.Cors.AllowHeaders
config.AllowMethods = conf.Conf.Cors.AllowMethods
Expand Down

0 comments on commit af3988f

Please sign in to comment.