small refactor*
This commit is contained in:
parent
b6b541e050
commit
24a4d30275
232 changed files with 2164 additions and 1906 deletions
plugins/qbittorrent
api.gocleanup.goclient.goconfig.godaemon.gofs.gogo.modgo.suminstall.goinstall_test.go
pkg/qbittorrent
application.goapplication_test.goauthentication.goauthentication_test.goclient.goclient_impl.goclient_test.gocommon.goconfig.goerror_code.golog.golog_test.gorss.gosearch.gosync.gosync_test.gotorrent.gotorrent_test.gotransfer.go
plugin
27
plugins/qbittorrent/api.go
Normal file
27
plugins/qbittorrent/api.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
)
|
||||
|
||||
func (d *Daemon) ListTorrents(ctx context.Context) ([]*qbittorrent.TorrentInfo, error) {
|
||||
return d.client.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{})
|
||||
}
|
||||
|
||||
func (d *Daemon) SourceFiles(ctx context.Context, hash string) ([]string, error) {
|
||||
d.sourceFilesMu.Lock()
|
||||
defer d.sourceFilesMu.Unlock()
|
||||
|
||||
out := make([]string, 0, 1)
|
||||
for k, h := range d.sourceFiles {
|
||||
if h != hash {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, k)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
120
plugins/qbittorrent/cleanup.go
Normal file
120
plugins/qbittorrent/cleanup.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/rlog"
|
||||
)
|
||||
|
||||
func (d *Daemon) Cleanup(ctx context.Context, run bool) ([]string, error) {
|
||||
d.log.Info(ctx, "cleanup started")
|
||||
|
||||
torrentInfos, err := d.client.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{})
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to get torrents", rlog.Error(err))
|
||||
return nil, fmt.Errorf("failed to get torrents: %w", err)
|
||||
}
|
||||
daemonsHashes := []string{}
|
||||
for _, info := range torrentInfos {
|
||||
daemonsHashes = append(daemonsHashes, info.Hash)
|
||||
}
|
||||
|
||||
dataDirs, err := os.ReadDir(d.dataDir)
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to read data directory", slog.String("path", d.dataDir), rlog.Error(err))
|
||||
return nil, fmt.Errorf("failed to read data directory: %w", err)
|
||||
}
|
||||
dataHashes := []string{}
|
||||
for _, entry := range dataDirs {
|
||||
dataHashes = append(dataHashes, entry.Name())
|
||||
}
|
||||
|
||||
hashToDelete := make([]string, 0, 5)
|
||||
|
||||
for _, v := range dataHashes {
|
||||
if !slices.Contains(daemonsHashes, v) {
|
||||
hashToDelete = append(hashToDelete, v)
|
||||
}
|
||||
}
|
||||
|
||||
d.log.Info(ctx, "marked torrents to delete",
|
||||
slog.Int("count", len(hashToDelete)),
|
||||
slog.Any("infohashes", hashToDelete),
|
||||
)
|
||||
|
||||
if !run {
|
||||
d.log.Info(ctx, "dry run, skipping deletion")
|
||||
return hashToDelete, nil
|
||||
}
|
||||
|
||||
for _, hash := range hashToDelete {
|
||||
d.log.Info(ctx, "deleting stale torrent data", slog.String("infohash", hash))
|
||||
err := os.RemoveAll(path.Join(d.dataDir, hash))
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to delete torrent data", slog.String("infohash", hash), rlog.Error(err))
|
||||
return nil, fmt.Errorf("failed to delete torrent data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return hashToDelete, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) CleanupUnregistred(ctx context.Context, run bool) ([]string, error) {
|
||||
d.log.Info(ctx, "cleanup started")
|
||||
|
||||
torrentInfos, err := d.client.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{})
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to get torrents", rlog.Error(err))
|
||||
return nil, fmt.Errorf("failed to get torrents: %w", err)
|
||||
}
|
||||
|
||||
torrentToDelete := make([]string, 0, 5)
|
||||
|
||||
for _, info := range torrentInfos {
|
||||
if d.registeredTorrents.Contains(info.Hash) {
|
||||
continue
|
||||
}
|
||||
|
||||
d.log.Info(ctx, "torrent not found in registry", slog.String("infohash", info.Hash))
|
||||
torrentToDelete = append(torrentToDelete, info.Hash)
|
||||
}
|
||||
|
||||
d.log.Info(ctx, "marked torrents to delete",
|
||||
slog.Int("count", len(torrentToDelete)),
|
||||
slog.Any("infohashes", torrentToDelete),
|
||||
)
|
||||
|
||||
if !run {
|
||||
d.log.Info(ctx, "dry run, skipping deletion")
|
||||
return torrentToDelete, nil
|
||||
}
|
||||
|
||||
err = d.client.qb.Torrent().DeleteTorrents(ctx, torrentToDelete, true)
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to delete torrents", slog.Any("infohashes", torrentToDelete), rlog.Error(err))
|
||||
return nil, fmt.Errorf("failed to delete torrents: %w", err)
|
||||
}
|
||||
d.log.Info(ctx, "torrents deleted from qbittorrent", slog.Int("count", len(torrentToDelete)))
|
||||
|
||||
for _, hash := range torrentToDelete {
|
||||
torrentPath := path.Join(d.dataDir, hash)
|
||||
_, err := os.Stat(torrentPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "failed to get torrent path", slog.String("path", torrentPath), rlog.Error(err))
|
||||
continue
|
||||
}
|
||||
d.log.Warn(ctx, "leftover data for torrent detected, cleaning up", slog.String("infohash", hash), slog.String("path", torrentPath))
|
||||
}
|
||||
|
||||
return torrentToDelete, nil
|
||||
}
|
273
plugins/qbittorrent/client.go
Normal file
273
plugins/qbittorrent/client.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"github.com/royalcat/btrgo/btrsync"
|
||||
"github.com/viccon/sturdyc"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
var meter = otel.Meter("git.kmsign.ru/royalcat/tstor/plugins/qbittorrent")
|
||||
|
||||
type cacheClient struct {
|
||||
qb qbittorrent.Client
|
||||
|
||||
propertiesCache *expirable.LRU[string, qbittorrent.TorrentProperties]
|
||||
|
||||
pieceCache btrsync.MapOf[pieceKey, int]
|
||||
|
||||
infoClient *sturdyc.Client[*qbittorrent.TorrentInfo]
|
||||
}
|
||||
|
||||
type pieceKey struct {
|
||||
hash string
|
||||
index int
|
||||
}
|
||||
|
||||
func wrapClient(qb qbittorrent.Client) *cacheClient {
|
||||
const (
|
||||
cacheSize = 5000
|
||||
cacheTTL = time.Minute
|
||||
)
|
||||
|
||||
return &cacheClient{
|
||||
qb: qb,
|
||||
propertiesCache: expirable.NewLRU[string, qbittorrent.TorrentProperties](cacheSize, nil, cacheTTL),
|
||||
|
||||
infoClient: sturdyc.New[*qbittorrent.TorrentInfo](cacheSize, 1, cacheTTL, 10,
|
||||
sturdyc.WithEarlyRefreshes(time.Minute, time.Minute*5, time.Second*10, time.Second*10),
|
||||
sturdyc.WithRefreshCoalescing(100, time.Second/4),
|
||||
sturdyc.WithMetrics(newSturdycMetrics()),
|
||||
),
|
||||
|
||||
pieceCache: btrsync.MapOf[pieceKey, int]{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *cacheClient) getInfo(ctx context.Context, hash string) (*qbittorrent.TorrentInfo, error) {
|
||||
out, err := f.infoClient.GetOrFetchBatch(ctx, []string{hash},
|
||||
f.infoClient.BatchKeyFn(""),
|
||||
func(ctx context.Context, ids []string) (map[string]*qbittorrent.TorrentInfo, error) {
|
||||
infos, err := f.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{
|
||||
Hashes: ids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error to get torrents: %w", err)
|
||||
}
|
||||
|
||||
out := make(map[string]*qbittorrent.TorrentInfo)
|
||||
for _, info := range infos {
|
||||
out[info.Hash] = info
|
||||
}
|
||||
|
||||
return out, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if out[hash] == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return out[hash], nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) getProperties(ctx context.Context, hash string) (*qbittorrent.TorrentProperties, error) {
|
||||
if v, ok := f.propertiesCache.Get(hash); ok {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
info, err := f.qb.Torrent().GetProperties(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f.propertiesCache.Add(hash, *info)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) listContent(ctx context.Context, hash string) ([]*qbittorrent.TorrentContent, error) {
|
||||
contents, err := f.qb.Torrent().GetContents(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) getContent(ctx context.Context, hash string, contentIndex int) (*qbittorrent.TorrentContent, error) {
|
||||
contents, err := f.qb.Torrent().GetContents(ctx, hash, contentIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentI := slices.IndexFunc(contents, func(c *qbittorrent.TorrentContent) bool {
|
||||
return c.Index == contentIndex
|
||||
})
|
||||
if contentI == -1 {
|
||||
return nil, fmt.Errorf("content not found")
|
||||
}
|
||||
|
||||
return contents[contentI], nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) isPieceComplete(ctx context.Context, hash string, pieceIndex int) (bool, error) {
|
||||
cachedPieceState, ok := f.pieceCache.Load(pieceKey{hash: hash, index: pieceIndex})
|
||||
if ok && cachedPieceState == 2 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
completion, err := f.qb.Torrent().GetPiecesStates(ctx, hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for i, v := range completion {
|
||||
f.pieceCache.Store(pieceKey{hash: hash, index: i}, v)
|
||||
}
|
||||
|
||||
if completion[pieceIndex] == 2 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (f *cacheClient) waitPieceToComplete(ctx context.Context, hash string, pieceIndex int) error {
|
||||
const checkingInterval = 1 * time.Second
|
||||
|
||||
ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(checkingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sturdycMetrics struct {
|
||||
ctx context.Context
|
||||
|
||||
cacheHit metric.Int64Counter
|
||||
cacheMiss metric.Int64Counter
|
||||
refresh metric.Int64Counter
|
||||
missing metric.Int64Counter
|
||||
forcedEviction metric.Int64Counter
|
||||
entryEviction metric.Int64Counter
|
||||
batchSize metric.Int64Histogram
|
||||
|
||||
observeCacheSize func() int
|
||||
}
|
||||
|
||||
var _ sturdyc.MetricsRecorder = (*sturdycMetrics)(nil)
|
||||
|
||||
func newSturdycMetrics() *sturdycMetrics {
|
||||
m := &sturdycMetrics{
|
||||
ctx: context.Background(),
|
||||
cacheHit: must(meter.Int64Counter("sturdyc_cache_hit")),
|
||||
cacheMiss: must(meter.Int64Counter("sturdyc_cache_miss")),
|
||||
refresh: must(meter.Int64Counter("sturdyc_cache_refresh")),
|
||||
missing: must(meter.Int64Counter("sturdyc_cache_missing")),
|
||||
forcedEviction: must(meter.Int64Counter("sturdyc_cache_forced_eviction")),
|
||||
entryEviction: must(meter.Int64Counter("sturdyc_cache_entry_eviction")),
|
||||
batchSize: must(meter.Int64Histogram("sturdyc_cache_batch_size")),
|
||||
}
|
||||
|
||||
must(meter.Int64ObservableGauge("sturdyc_cache_size",
|
||||
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
|
||||
if m.observeCacheSize == nil {
|
||||
return nil
|
||||
}
|
||||
io.Observe(int64(m.observeCacheSize()))
|
||||
return nil
|
||||
})))
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) CacheHit() {
|
||||
s.cacheHit.Add(s.ctx, 1)
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) CacheMiss() {
|
||||
s.cacheMiss.Add(s.ctx, 1)
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) Refresh() {
|
||||
s.refresh.Add(s.ctx, 1)
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) MissingRecord() {
|
||||
s.missing.Add(s.ctx, 1)
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) ForcedEviction() {
|
||||
s.forcedEviction.Add(s.ctx, 1)
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) CacheBatchRefreshSize(size int) {
|
||||
s.batchSize.Record(s.ctx, int64(size))
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) ObserveCacheSize(callback func() int) {
|
||||
s.observeCacheSize = callback
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) EntriesEvicted(evictd int) {
|
||||
s.entryEviction.Add(s.ctx, int64(evictd))
|
||||
}
|
||||
|
||||
// AsynchronousRefresh implements sturdyc.MetricsRecorder.
|
||||
func (s *sturdycMetrics) AsynchronousRefresh() {
|
||||
return
|
||||
}
|
||||
|
||||
// SynchronousRefresh implements sturdyc.MetricsRecorder.
|
||||
func (s *sturdycMetrics) SynchronousRefresh() {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sturdycMetrics) ShardIndex(int) {
|
||||
return
|
||||
}
|
||||
|
||||
func must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
74
plugins/qbittorrent/config.go
Normal file
74
plugins/qbittorrent/config.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"github.com/knadh/koanf/providers/structs"
|
||||
"github.com/knadh/koanf/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DataDir string `koanf:"data_dir"`
|
||||
MetadataDir string `koanf:"metadata_dir"`
|
||||
}
|
||||
|
||||
var defaultConfig = Config{
|
||||
DataDir: "/data/qbittorrent/data",
|
||||
MetadataDir: "/data/qbittorrent/metadata",
|
||||
}
|
||||
|
||||
func loadConfig(k *koanf.Koanf) (Config, error) {
|
||||
koanf := koanf.New(".")
|
||||
|
||||
err := koanf.Load(structs.Provider(defaultConfig, "koanf"), nil)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
err = koanf.Merge(k)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// var defaultRoutes = []Route{
|
||||
// {
|
||||
// Name: "multimedia",
|
||||
// Torrents: []Torrent{
|
||||
// {
|
||||
// MagnetURI: "magnet:?xt=urn:btih:c9e15763f722f23e98a29decdfae341b98d53056&dn=Cosmos+Laundromat&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fcosmos-laundromat.torrent",
|
||||
// },
|
||||
// {
|
||||
// MagnetURI: "magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fbig-buck-bunny.torrent",
|
||||
// },
|
||||
// {
|
||||
// MagnetURI: "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent",
|
||||
// },
|
||||
// {
|
||||
// MagnetURI: "magnet:?xt=urn:btih:209c8226b299b308beaf2b9cd3fb49212dbd13ec&dn=Tears+of+Steel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Ftears-of-steel.torrent",
|
||||
// },
|
||||
// {
|
||||
// MagnetURI: "magnet:?xt=urn:btih:a88fda5954e89178c372716a6a78b8180ed4dad3&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fwired-cd.torrent",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// var defaultServers = []Server{
|
||||
// {
|
||||
// Name: "server",
|
||||
// Path: "server",
|
||||
// Trackers: []string{
|
||||
// "wss://tracker.btorrent.xyz",
|
||||
// "wss://tracker.openwebtorrent.com",
|
||||
// "http://p4p.arenabg.com:1337/announce",
|
||||
// "udp://tracker.opentrackr.org:1337/announce",
|
||||
// "udp://open.tracker.cl:1337/announce",
|
||||
// "http://openbittorrent.com:80/announce",
|
||||
// },
|
||||
// },
|
||||
// }
|
306
plugins/qbittorrent/daemon.go
Normal file
306
plugins/qbittorrent/daemon.go
Normal file
|
@ -0,0 +1,306 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/rlog"
|
||||
"git.kmsign.ru/royalcat/tstor/server/src/daemon"
|
||||
"git.kmsign.ru/royalcat/tstor/server/src/logwrap"
|
||||
"git.kmsign.ru/royalcat/tstor/server/src/vfs"
|
||||
"git.kmsign.ru/royalcat/tstor/server/tstor"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/types/infohash"
|
||||
infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/royalcat/ctxio"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/plugins/qbittorrent")
|
||||
|
||||
type Daemon struct {
|
||||
proc *os.Process
|
||||
qb qbittorrent.Client
|
||||
client *cacheClient
|
||||
|
||||
sourceFilesMu sync.Mutex
|
||||
sourceFiles map[string]string // [sourcePath]infohash
|
||||
|
||||
registeredTorrents mapset.Set[string] // infohash list
|
||||
|
||||
dataDir string
|
||||
log *rlog.Logger
|
||||
}
|
||||
|
||||
const defaultConf = `
|
||||
[LegalNotice]
|
||||
Accepted=true
|
||||
|
||||
[Preferences]
|
||||
WebUI\LocalHostAuth=false
|
||||
WebUI\Password_PBKDF2="@ByteArray(qef5I4wZBkDG+PP6/5mQwA==:LoTmorQM/QM5RHI4+dOiu6xfAz9xak6fhR4ZGpRtJF3JNCGG081Yrtva4G71kXz//ODUuWQKTLlrZPuIDvzqUQ==)"
|
||||
`
|
||||
|
||||
var Plugin = &tstor.Plugin{
|
||||
Name: DaemonName,
|
||||
DaemonConstructor: NewDaemon,
|
||||
}
|
||||
|
||||
const DaemonName = "qbittorrent"
|
||||
|
||||
var _ daemon.DaemonConstructor = NewDaemon
|
||||
|
||||
func NewDaemon(ctx context.Context, koanf *koanf.Koanf) (daemon.Daemon, error) {
|
||||
log := rlog.Component(DaemonName)
|
||||
|
||||
log.Debug(ctx, "QBittorrent plugin loaded. Starting qbittorrent-nox")
|
||||
|
||||
config, err := loadConfig(koanf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
binPath := config.MetadataDir + "/qbittorrent-nox"
|
||||
err = downloadLatestQbitRelease(ctx, binPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
daemonLog := log.WithComponent("process")
|
||||
outLog := logwrap.NewSlogWriter(ctx, slog.LevelInfo, daemonLog.Slog())
|
||||
errLog := logwrap.NewSlogWriter(ctx, slog.LevelError, daemonLog.Slog())
|
||||
|
||||
_, err = os.Stat(config.MetadataDir + "/profile/qBittorrent/config/qBittorrent.conf")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
err = os.MkdirAll(config.MetadataDir+"/profile/qBittorrent/config", 0744)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = os.WriteFile(config.MetadataDir+"/profile/qBittorrent/config/qBittorrent.conf", []byte(defaultConf), 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(config.DataDir, 0744)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const port = 25436
|
||||
|
||||
proc, err := runQBittorrent(binPath, config.MetadataDir+"/profile", port, outLog, errLog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
qb, err := qbittorrent.NewClient(ctx, &qbittorrent.Config{
|
||||
Address: fmt.Sprintf("http://localhost:%d", port),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for { // wait for qbittorrent to start
|
||||
ver, err := qb.Application().Version(ctx)
|
||||
log.Info(ctx, "qbittorrent started", slog.String("version", ver))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
log.Warn(ctx, "waiting for qbittorrent to start", rlog.Error(err))
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
dataDir, err := filepath.Abs(config.DataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = qb.Application().SetPreferences(ctx, &qbittorrent.Preferences{
|
||||
SavePath: dataDir,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Daemon{
|
||||
qb: qb,
|
||||
proc: proc,
|
||||
dataDir: config.DataDir,
|
||||
sourceFiles: make(map[string]string),
|
||||
registeredTorrents: mapset.NewSet[string](),
|
||||
client: wrapClient(qb),
|
||||
log: rlog.Component(DaemonName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) Name() string {
|
||||
return DaemonName
|
||||
}
|
||||
|
||||
func (d *Daemon) Extensions() []string {
|
||||
return []string{".torrent"}
|
||||
}
|
||||
|
||||
func (d *Daemon) Close(ctx context.Context) error {
|
||||
err := d.proc.Signal(os.Interrupt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d.proc.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func torrentDataPath(dataDir string, ih string) (string, error) {
|
||||
return filepath.Abs(path.Join(dataDir, ih))
|
||||
}
|
||||
|
||||
func (fs *Daemon) GetFS(ctx context.Context, sourcePath string, file vfs.File) (vfs.Filesystem, error) {
|
||||
ctx, span := trace.Start(ctx, "GetTorrentFS")
|
||||
defer span.End()
|
||||
|
||||
stat, err := file.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := fs.log.With(slog.String("file", file.Name()))
|
||||
|
||||
ih, err := readInfoHash(ctx, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log = log.With(slog.String("infohash", ih.HexString()))
|
||||
|
||||
torrentPath, err := torrentDataPath(fs.dataDir, ih.HexString())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting torrent path: %w", err)
|
||||
}
|
||||
log = log.With(slog.String("torrentPath", torrentPath))
|
||||
|
||||
log.Debug(ctx, "creating fs for torrent")
|
||||
|
||||
err = fs.syncTorrentState(ctx, file, ih, torrentPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error syncing torrent state: %w", err)
|
||||
}
|
||||
|
||||
fs.sourceFilesMu.Lock()
|
||||
fs.sourceFiles[sourcePath] = ih.HexString()
|
||||
fs.sourceFilesMu.Unlock()
|
||||
|
||||
return newTorrentFS(ctx, fs.client, file.Name(), ih.HexString(), stat.ModTime(), torrentPath)
|
||||
}
|
||||
|
||||
func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainfo.Hash, torrentPath string) error {
|
||||
ctx, span := trace.Start(ctx, "syncTorrentState")
|
||||
defer span.End()
|
||||
log := d.log.With(slog.String("file", file.Name()), slog.String("infohash", ih.HexString()))
|
||||
|
||||
info, err := d.client.getInfo(ctx, ih.HexString())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log = log.With(slog.String("torrentPath", torrentPath))
|
||||
|
||||
if info == nil {
|
||||
_, err := file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := ctxio.ReadAll(ctx, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = d.qb.Torrent().AddNewTorrent(ctx, &qbittorrent.TorrentAddOption{
|
||||
Torrents: []*qbittorrent.TorrentAddFileMetadata{
|
||||
{
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
SavePath: torrentPath,
|
||||
// SequentialDownload: "true",
|
||||
FirstLastPiecePrio: "true",
|
||||
})
|
||||
if err != nil {
|
||||
d.log.Error(ctx, "error adding torrent", rlog.Error(err))
|
||||
return fmt.Errorf("error adding torrent: %w", err)
|
||||
}
|
||||
|
||||
var props *qbittorrent.TorrentProperties
|
||||
for {
|
||||
props, err = d.client.getProperties(ctx, ih.HexString())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
log.Error(ctx, "waiting for torrent to be added", rlog.Error(err))
|
||||
time.Sleep(time.Millisecond * 15)
|
||||
}
|
||||
|
||||
log.Info(ctx, "added torrent", slog.String("infohash", ih.HexString()))
|
||||
|
||||
d.registeredTorrents.Add(props.Hash)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
// info := existing[0]
|
||||
props, err := d.client.getProperties(ctx, ih.HexString())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting torrent properties: %w for infohash: %s", err, ih.HexString())
|
||||
}
|
||||
|
||||
d.registeredTorrents.Add(props.Hash)
|
||||
|
||||
if props.SavePath != torrentPath {
|
||||
log.Info(ctx, "moving torrent to correct location", slog.String("oldPath", props.SavePath))
|
||||
err = d.qb.Torrent().SetLocation(ctx, []string{ih.HexString()}, torrentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error moving torrent: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO caching
|
||||
func readInfoHash(ctx context.Context, file vfs.File) (infohash.T, error) {
|
||||
mi, err := metainfo.Load(ctxio.IoReader(ctx, file))
|
||||
if err != nil {
|
||||
return infohash.T{}, err
|
||||
}
|
||||
info, err := mi.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return infohash.T{}, err
|
||||
}
|
||||
|
||||
if info.HasV2() {
|
||||
ih := infohash_v2.HashBytes(mi.InfoBytes)
|
||||
return *(&ih).ToShort(), nil
|
||||
}
|
||||
|
||||
return infohash.HashBytes(mi.InfoBytes), nil
|
||||
}
|
410
plugins/qbittorrent/fs.go
Normal file
410
plugins/qbittorrent/fs.go
Normal file
|
@ -0,0 +1,410 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
|
||||
"git.kmsign.ru/royalcat/tstor/server/pkg/rlog"
|
||||
"git.kmsign.ru/royalcat/tstor/server/src/vfs"
|
||||
)
|
||||
|
||||
type FS struct {
|
||||
mu sync.Mutex
|
||||
client *cacheClient
|
||||
name string
|
||||
hash string
|
||||
dataDir string // directory where torrent files are stored
|
||||
modTime time.Time
|
||||
|
||||
entries map[string]fileEntry
|
||||
|
||||
log *rlog.Logger
|
||||
|
||||
vfs.FilesystemPrototype
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
fs.FileInfo
|
||||
Content *qbittorrent.TorrentContent
|
||||
}
|
||||
|
||||
var _ vfs.Filesystem = (*FS)(nil)
|
||||
|
||||
func newTorrentFS(ctx context.Context, client *cacheClient, name string, hash string, modTime time.Time, dataDir string) (*FS, error) {
|
||||
ctx, span := trace.Start(ctx, "newTorrentFS")
|
||||
defer span.End()
|
||||
|
||||
cnts, err := client.listContent(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list content for hash %s: %w", hash, err)
|
||||
}
|
||||
|
||||
entries := make(map[string]fileEntry, len(cnts))
|
||||
for _, cnt := range cnts {
|
||||
if cnt.Priority == qbittorrent.PriorityDoNotDownload {
|
||||
continue
|
||||
}
|
||||
|
||||
entries[vfs.AbsPath(cnt.Name)] = fileEntry{
|
||||
Content: cnt,
|
||||
FileInfo: vfs.NewFileInfo(cnt.Name, cnt.Size, modTime),
|
||||
}
|
||||
}
|
||||
|
||||
return &FS{
|
||||
client: client,
|
||||
name: name,
|
||||
hash: hash,
|
||||
modTime: modTime,
|
||||
|
||||
dataDir: dataDir,
|
||||
|
||||
entries: entries,
|
||||
|
||||
log: rlog.Component("qbittorrent", "fs"),
|
||||
|
||||
FilesystemPrototype: vfs.FilesystemPrototype(name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Open implements vfs.Filesystem.
|
||||
func (f *FS) Open(ctx context.Context, name string) (vfs.File, error) {
|
||||
if name == vfs.Separator {
|
||||
return vfs.NewDirFile(name), nil
|
||||
}
|
||||
|
||||
if entry, ok := f.entries[name]; ok {
|
||||
return openFile(ctx, f.client, f.dataDir, f.hash, entry.Content)
|
||||
}
|
||||
|
||||
for p := range f.entries {
|
||||
if strings.HasPrefix(p, name) {
|
||||
return vfs.NewDirFile(name), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, vfs.ErrNotExist
|
||||
}
|
||||
|
||||
// ReadDir implements vfs.Filesystem.
|
||||
func (f *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
||||
infos := make(map[string]fs.FileInfo, len(f.entries))
|
||||
for k, v := range f.entries {
|
||||
infos[k] = v.FileInfo
|
||||
}
|
||||
|
||||
return vfs.ListDirFromInfo(infos, name)
|
||||
}
|
||||
|
||||
// Stat implements vfs.Filesystem.
|
||||
func (f *FS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
name = vfs.AbsPath(path.Clean(name))
|
||||
|
||||
if vfs.IsRoot(name) {
|
||||
return vfs.NewDirInfo(f.name, f.modTime), nil
|
||||
}
|
||||
|
||||
if entry, ok := f.entries[name]; ok {
|
||||
return entry.FileInfo, nil
|
||||
}
|
||||
|
||||
for p := range f.entries {
|
||||
if strings.HasPrefix(p, name) {
|
||||
return vfs.NewDirInfo(name, f.modTime), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, vfs.ErrNotExist
|
||||
}
|
||||
|
||||
// Unlink implements vfs.Filesystem.
|
||||
func (f *FS) Unlink(ctx context.Context, filename string) error {
|
||||
filename = vfs.AbsPath(path.Clean(filename))
|
||||
|
||||
// we cannot delete a torrent itself, cause it will be added on next source scan and all delited files will be restored
|
||||
|
||||
if entry, ok := f.entries[filename]; ok {
|
||||
return f.removeFile(ctx, f.hash, entry.Content)
|
||||
}
|
||||
|
||||
for p, entry := range f.entries {
|
||||
if strings.HasPrefix(p, filename) {
|
||||
return f.removeFile(ctx, f.hash, entry.Content)
|
||||
}
|
||||
}
|
||||
|
||||
return vfs.ErrNotExist
|
||||
}
|
||||
|
||||
func (f *FS) Rename(ctx context.Context, oldpath string, newpath string) error {
|
||||
oldpath = vfs.AbsPath(path.Clean(oldpath))
|
||||
newpath = vfs.AbsPath(path.Clean(newpath))
|
||||
|
||||
if _, ok := f.entries[oldpath]; ok {
|
||||
err := f.client.qb.Torrent().RenameFile(ctx, f.hash, vfs.RelPath(oldpath), vfs.RelPath(newpath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename file %s to %s: %w", oldpath, newpath, err)
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.entries[newpath] = f.entries[oldpath]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return vfs.ErrNotExist
|
||||
}
|
||||
|
||||
func (f *FS) removeFile(ctx context.Context, hash string, content *qbittorrent.TorrentContent) error {
|
||||
log := f.log.With(slog.String("hash", hash), slog.String("file", content.Name))
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
fpath := vfs.AbsPath(content.Name)
|
||||
|
||||
if _, ok := f.entries[fpath]; !ok {
|
||||
return fmt.Errorf("file %s is does not found", fpath)
|
||||
}
|
||||
delete(f.entries, fpath)
|
||||
|
||||
err := f.client.qb.Torrent().SetFilePriority(ctx, f.hash, content.Index, qbittorrent.PriorityDoNotDownload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set priority for torrent %s for file %s: %w", hash, content.Name, err)
|
||||
}
|
||||
|
||||
err = os.Remove(path.Join(f.dataDir, vfs.RelPath(content.Name)))
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
log.Warn(ctx, "failed to remove file", rlog.Error(err))
|
||||
return fmt.Errorf("failed to remove file %s: %w", content.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func openFile(ctx context.Context, client *cacheClient, torrentDir string, hash string, content *qbittorrent.TorrentContent) (*File, error) {
|
||||
props, err := client.getProperties(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME error when file not started downloading
|
||||
file, err := os.OpenFile(path.Join(torrentDir, content.Name), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &File{
|
||||
client: client,
|
||||
hash: hash,
|
||||
torrentDir: torrentDir,
|
||||
|
||||
filePath: content.Name,
|
||||
contentIndex: content.Index,
|
||||
pieceSize: props.PieceSize,
|
||||
fileSize: content.Size,
|
||||
|
||||
file: file,
|
||||
|
||||
offset: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
client *cacheClient
|
||||
hash string
|
||||
torrentModTime time.Time
|
||||
torrentDir string
|
||||
filePath string // path inside a torrent directory
|
||||
contentIndex int
|
||||
pieceSize int
|
||||
fileSize int64
|
||||
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
offset int64
|
||||
}
|
||||
|
||||
var _ vfs.File = (*File)(nil)
|
||||
|
||||
// Info implements vfs.File.
|
||||
func (f *File) Info() (fs.FileInfo, error) {
|
||||
return vfs.NewFileInfo(path.Base(f.filePath), f.fileSize, f.torrentModTime), nil
|
||||
}
|
||||
|
||||
// IsDir implements vfs.File.
|
||||
func (f *File) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Seek implements vfs.File.
|
||||
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
f.offset = offset
|
||||
case io.SeekCurrent:
|
||||
f.offset += offset
|
||||
case io.SeekEnd:
|
||||
f.offset = f.fileSize + offset
|
||||
}
|
||||
return f.offset, nil
|
||||
}
|
||||
|
||||
// Name implements vfs.File.
|
||||
func (f *File) Name() string {
|
||||
return path.Base(f.filePath)
|
||||
}
|
||||
|
||||
func (f *File) canExpectSoon(ctx context.Context) (bool, error) {
|
||||
info, err := f.client.getInfo(ctx, f.hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if info == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return info.Completed == info.Size || info.State == qbittorrent.TorrentStateCheckingUP || info.State == qbittorrent.TorrentStateDownloading || info.State == qbittorrent.TorrentStateForcedDL, nil
|
||||
}
|
||||
|
||||
func (f *File) isRangeComplete(ctx context.Context, offset int64, size int) (bool, error) {
|
||||
startPieceIndex := int(offset / int64(f.pieceSize))
|
||||
pieceCount := (size + f.pieceSize - 1) / f.pieceSize // rouding up
|
||||
|
||||
for i := range pieceCount {
|
||||
ok, err := f.client.isPieceComplete(ctx, f.hash, startPieceIndex+i)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *File) waitRangeAvailable(ctx context.Context, offset int64, size int) error {
|
||||
complete, err := f.isRangeComplete(ctx, offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
|
||||
canExpectSoon, err := f.canExpectSoon(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canExpectSoon {
|
||||
return fmt.Errorf("torrent is not downloading")
|
||||
}
|
||||
|
||||
const checkingInterval = 1 * time.Second
|
||||
|
||||
ticker := time.NewTicker(checkingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
complete, err := f.isRangeComplete(ctx, offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read implements vfs.File.
|
||||
func (f *File) Read(ctx context.Context, p []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if err := f.waitRangeAvailable(ctx, f.offset, len(p)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := f.file.ReadAt(p, f.offset)
|
||||
f.offset += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadAt implements vfs.File.
|
||||
func (f *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
|
||||
if err := f.waitRangeAvailable(ctx, f.offset, len(p)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return f.file.ReadAt(p, off)
|
||||
}
|
||||
|
||||
// Size implements vfs.File.
|
||||
func (f *File) Size() int64 {
|
||||
return f.fileSize
|
||||
}
|
||||
|
||||
// Type implements vfs.File.
|
||||
func (f *File) Type() fs.FileMode {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
// Close implements vfs.File.
|
||||
func (f *File) Close(ctx context.Context) error {
|
||||
return f.file.Close()
|
||||
}
|
||||
|
||||
// type fileInfo struct {
|
||||
// name string
|
||||
// size int64
|
||||
// modTime time.Time
|
||||
// }
|
||||
|
||||
// var _ fs.FileInfo = (*fileInfo)(nil)
|
||||
|
||||
// // IsDir implements fs.FileInfo.
|
||||
// func (f *fileInfo) IsDir() bool {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// // ModTime implements fs.FileInfo.
|
||||
// func (f *fileInfo) ModTime() time.Time {
|
||||
// return f.modTime
|
||||
// }
|
||||
|
||||
// // Mode implements fs.FileInfo.
|
||||
// func (f *fileInfo) Mode() fs.FileMode {
|
||||
// return vfs.ModeFileRO
|
||||
// }
|
||||
|
||||
// // Name implements fs.FileInfo.
|
||||
// func (f *fileInfo) Name() string {
|
||||
// return f.name
|
||||
// }
|
||||
|
||||
// // Size implements fs.FileInfo.
|
||||
// func (f *fileInfo) Size() int64 {
|
||||
// return f.size
|
||||
// }
|
||||
|
||||
// // Sys implements fs.FileInfo.
|
||||
// func (f *fileInfo) Sys() any {
|
||||
// return nil
|
||||
// }
|
56
plugins/qbittorrent/go.mod
Normal file
56
plugins/qbittorrent/go.mod
Normal file
|
@ -0,0 +1,56 @@
|
|||
module git.kmsign.ru/royalcat/tstor/plugins/qbittorrent
|
||||
|
||||
go 1.23.5
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
replace github.com/iceber/iouring-go => github.com/royalcat/iouring-go v0.0.0-20240925200811-286062ac1b23
|
||||
|
||||
require (
|
||||
github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845
|
||||
github.com/deckarep/golang-set/v2 v2.7.0
|
||||
github.com/google/go-github/v63 v63.0.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/knadh/koanf/providers/structs v0.1.0
|
||||
github.com/knadh/koanf/v2 v2.1.2
|
||||
github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450
|
||||
github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/viccon/sturdyc v1.1.1
|
||||
go.opentelemetry.io/otel v1.34.0
|
||||
go.opentelemetry.io/otel/metric v1.34.0
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
|
||||
golang.org/x/sys v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.7.4 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.1.6 // indirect
|
||||
)
|
332
plugins/qbittorrent/go.sum
Normal file
332
plugins/qbittorrent/go.sum
Normal file
|
@ -0,0 +1,332 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anacrolix/dht/v2 v2.22.0 h1:wat5FLdT25vltHsjX377GBrpK9o6L2QVn541bIguCYo=
|
||||
github.com/anacrolix/dht/v2 v2.22.0/go.mod h1:shbBjhgvezqsJoE+hMo/ezHYQFF18V9jUllNIP5xV9k=
|
||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca h1:aiiGqSQWjtVNdi8zUMfA//IrM8fPkv2bWwZVPbDe0wg=
|
||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8=
|
||||
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||
github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||
github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
|
||||
github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw=
|
||||
github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc=
|
||||
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
||||
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
||||
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.4 h1:47h5OXoPV8JbA/ACA+FLwKdYbAinuDO8osc2Cu9xkxg=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.4/go.mod h1:vVO5FEziQm+NFmJesc7StpkquZk+WJFCaL0Wp//2sa0=
|
||||
github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
|
||||
github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
|
||||
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
||||
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845 h1:ZuYsqgbLCVJHHmYQKG6ImMtz+3hUOI1qvRJTuxTVEZY=
|
||||
github.com/anacrolix/torrent v1.58.1-0.20241228235504-75e6b6565845/go.mod h1:n3SjHIE8oHXeH0Px0d5FXQ7cU4IgbEfTroen6B9KWJk=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
|
||||
github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
|
||||
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0=
|
||||
github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
|
||||
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
|
||||
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
||||
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450 h1:AZyZxXZLniAR0DaZhTS4RVcHtOvYMW8IunplqC9A0mk=
|
||||
github.com/royalcat/btrgo v0.0.0-20240318160410-19bd27154450/go.mod h1:m3TPa9l/wMKpm/7WHrMs3dSFUxo7kLHaI8ap+SFGYhQ=
|
||||
github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176 h1:2jCQJow6jRvhpdMJCo1Okd7tq5Rg4YXlUxqT0q0NWAg=
|
||||
github.com/royalcat/ctxio v0.0.0-20240602084623-009bd79b3176/go.mod h1:81eB8eOH/UU7pzI7J1Rsg3KLpshF7BXg4+UHbex+27I=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/viccon/sturdyc v1.1.1 h1:ZrAdlQHDhkE2zwTLH/948vKCuaEOXGP3ezKDWh8xOHw=
|
||||
github.com/viccon/sturdyc v1.1.1/go.mod h1:OCBEgG/i48uugKQ498UQlfMHmf5j8MYY8a4BApfVnMo=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
150
plugins/qbittorrent/install.go
Normal file
150
plugins/qbittorrent/install.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v63/github"
|
||||
"golang.org/x/sys/cpu"
|
||||
)
|
||||
|
||||
const (
|
||||
repoOwner = "userdocs"
|
||||
repoName = "qbittorrent-nox-static"
|
||||
)
|
||||
|
||||
func runQBittorrent(binPath string, profileDir string, port int, stdout, stderr io.Writer) (*os.Process, error) {
|
||||
err := os.Chmod(binPath, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, fmt.Sprintf("--profile=%s", profileDir), fmt.Sprintf("--webui-port=%d", port))
|
||||
cmd.Stdin = bytes.NewReader([]byte("y\n"))
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd.Process, nil
|
||||
}
|
||||
|
||||
func downloadLatestQbitRelease(ctx context.Context, binPath string) error {
|
||||
client := github.NewClient(nil)
|
||||
rel, _, err := client.Repositories.GetLatestRelease(ctx, repoOwner, repoName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arch := ""
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
arch = "x86_64"
|
||||
case "arm":
|
||||
arch = "armhf" // this is a safe version, go does not distinguish between armv6 and armv7
|
||||
if cpu.ARM.HasNEON {
|
||||
arch = "armv7"
|
||||
}
|
||||
case "arm64":
|
||||
arch = "aarch64"
|
||||
}
|
||||
|
||||
if arch == "" {
|
||||
return errors.New("unsupported architecture")
|
||||
}
|
||||
|
||||
binName := arch + "-qbittorrent-nox"
|
||||
|
||||
var targetRelease *github.ReleaseAsset
|
||||
for _, v := range rel.Assets {
|
||||
if v.GetName() == binName {
|
||||
targetRelease = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetRelease == nil {
|
||||
return fmt.Errorf("target asset %s not found", binName)
|
||||
}
|
||||
|
||||
downloadUrl := targetRelease.GetBrowserDownloadURL()
|
||||
if downloadUrl == "" {
|
||||
return errors.New("download url is empty")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(path.Dir(binPath), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "downloading latest qbittorrent-nox release", slog.String("url", downloadUrl))
|
||||
|
||||
return downloadFile(ctx, binPath, downloadUrl)
|
||||
}
|
||||
|
||||
func downloadFile(ctx context.Context, filepath string, webUrl string) error {
|
||||
if stat, err := os.Stat(filepath); err == nil {
|
||||
resp, err := http.Head(webUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var lastModified time.Time
|
||||
|
||||
lastModifiedHeader := resp.Header.Get("Last-Modified")
|
||||
if lastModifiedHeader != "" {
|
||||
lastModified, err = time.Parse(http.TimeFormat, lastModifiedHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if resp.ContentLength == stat.Size() && lastModified.Before(stat.ModTime()) {
|
||||
slog.InfoContext(ctx, "there is already newest version of the file", slog.String("filepath", filepath))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, webUrl, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the data
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Writer the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
18
plugins/qbittorrent/install_test.go
Normal file
18
plugins/qbittorrent/install_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDownloadQBittorent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tempDir := t.TempDir()
|
||||
require := require.New(t)
|
||||
err := downloadLatestQbitRelease(ctx, tempDir)
|
||||
require.NoError(err)
|
||||
err = downloadLatestQbitRelease(ctx, tempDir)
|
||||
require.NoError(err)
|
||||
}
|
396
plugins/qbittorrent/pkg/qbittorrent/application.go
Normal file
396
plugins/qbittorrent/pkg/qbittorrent/application.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Application interface {
|
||||
// Version get application version
|
||||
Version(context.Context) (string, error)
|
||||
// WebApiVersion get webapi version
|
||||
WebApiVersion(context.Context) (string, error)
|
||||
// BuildInfo get build info
|
||||
BuildInfo(context.Context) (*BuildInfo, error)
|
||||
// Shutdown exit application
|
||||
Shutdown(context.Context) error
|
||||
// GetPreferences get application preferences
|
||||
GetPreferences(context.Context) (*Preferences, error)
|
||||
// SetPreferences set application preferences
|
||||
SetPreferences(context.Context, *Preferences) error
|
||||
// DefaultSavePath get default save path
|
||||
DefaultSavePath(context.Context) (string, error)
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
BitNess int `json:"bitness,omitempty"`
|
||||
Boost string `json:"boost,omitempty"`
|
||||
LibTorrent string `json:"libtorrent,omitempty"`
|
||||
Openssl string `json:"openssl,omitempty"`
|
||||
QT string `json:"qt,omitempty"`
|
||||
Zlib string `json:"zlib,omitempty"`
|
||||
}
|
||||
|
||||
type Preferences struct {
|
||||
AddToTopOfQueue bool `json:"add_to_top_of_queue,omitempty"`
|
||||
AddTrackers string `json:"add_trackers,omitempty"`
|
||||
AddTrackersEnabled bool `json:"add_trackers_enabled,omitempty"`
|
||||
AltDlLimit int `json:"alt_dl_limit,omitempty"`
|
||||
AltUpLimit int `json:"alt_up_limit,omitempty"`
|
||||
AlternativeWebuiEnabled bool `json:"alternative_webui_enabled,omitempty"`
|
||||
AlternativeWebuiPath string `json:"alternative_webui_path,omitempty"`
|
||||
AnnounceIP string `json:"announce_ip,omitempty"`
|
||||
AnnounceToAllTiers bool `json:"announce_to_all_tiers,omitempty"`
|
||||
AnnounceToAllTrackers bool `json:"announce_to_all_trackers,omitempty"`
|
||||
AnonymousMode bool `json:"anonymous_mode,omitempty"`
|
||||
AsyncIoThreads int `json:"async_io_threads,omitempty"`
|
||||
AutoDeleteMode int `json:"auto_delete_mode,omitempty"`
|
||||
AutoTmmEnabled bool `json:"auto_tmm_enabled,omitempty"`
|
||||
AutorunEnabled bool `json:"autorun_enabled,omitempty"`
|
||||
AutorunOnTorrentAddedEnabled bool `json:"autorun_on_torrent_added_enabled,omitempty"`
|
||||
AutorunOnTorrentAddedProgram string `json:"autorun_on_torrent_added_program,omitempty"`
|
||||
AutorunProgram string `json:"autorun_program,omitempty"`
|
||||
BannedIPs string `json:"banned_IPs,omitempty"`
|
||||
BdecodeDepthLimit int `json:"bdecode_depth_limit,omitempty"`
|
||||
BdecodeTokenLimit int `json:"bdecode_token_limit,omitempty"`
|
||||
BittorrentProtocol int `json:"bittorrent_protocol,omitempty"`
|
||||
BlockPeersOnPrivilegedPorts bool `json:"block_peers_on_privileged_ports,omitempty"`
|
||||
BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist,omitempty"`
|
||||
BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled,omitempty"`
|
||||
BypassLocalAuth bool `json:"bypass_local_auth,omitempty"`
|
||||
CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled,omitempty"`
|
||||
CheckingMemoryUse int `json:"checking_memory_use,omitempty"`
|
||||
ConnectionSpeed int `json:"connection_speed,omitempty"`
|
||||
CurrentInterfaceAddress string `json:"current_interface_address,omitempty"`
|
||||
CurrentInterfaceName string `json:"current_interface_name,omitempty"`
|
||||
CurrentNetworkInterface string `json:"current_network_interface,omitempty"`
|
||||
Dht bool `json:"dht,omitempty"`
|
||||
DiskCache int `json:"disk_cache,omitempty"`
|
||||
DiskCacheTTL int `json:"disk_cache_ttl,omitempty"`
|
||||
DiskIoReadMode int `json:"disk_io_read_mode,omitempty"`
|
||||
DiskIoType int `json:"disk_io_type,omitempty"`
|
||||
DiskIoWriteMode int `json:"disk_io_write_mode,omitempty"`
|
||||
DiskQueueSize int `json:"disk_queue_size,omitempty"`
|
||||
DlLimit int `json:"dl_limit,omitempty"`
|
||||
DontCountSlowTorrents bool `json:"dont_count_slow_torrents,omitempty"`
|
||||
DyndnsDomain string `json:"dyndns_domain,omitempty"`
|
||||
DyndnsEnabled bool `json:"dyndns_enabled,omitempty"`
|
||||
DyndnsPassword string `json:"dyndns_password,omitempty"`
|
||||
DyndnsService int `json:"dyndns_service,omitempty"`
|
||||
DyndnsUsername string `json:"dyndns_username,omitempty"`
|
||||
EmbeddedTrackerPort int `json:"embedded_tracker_port,omitempty"`
|
||||
EmbeddedTrackerPortForwarding bool `json:"embedded_tracker_port_forwarding,omitempty"`
|
||||
EnableCoalesceReadWrite bool `json:"enable_coalesce_read_write,omitempty"`
|
||||
EnableEmbeddedTracker bool `json:"enable_embedded_tracker,omitempty"`
|
||||
EnableMultiConnectionsFromSameIP bool `json:"enable_multi_connections_from_same_ip,omitempty"`
|
||||
EnablePieceExtentAffinity bool `json:"enable_piece_extent_affinity,omitempty"`
|
||||
EnableUploadSuggestions bool `json:"enable_upload_suggestions,omitempty"`
|
||||
Encryption int `json:"encryption,omitempty"`
|
||||
ExcludedFileNames string `json:"excluded_file_names,omitempty"`
|
||||
ExcludedFileNamesEnabled bool `json:"excluded_file_names_enabled,omitempty"`
|
||||
ExportDir string `json:"export_dir,omitempty"`
|
||||
ExportDirFin string `json:"export_dir_fin,omitempty"`
|
||||
FileLogAge int `json:"file_log_age,omitempty"`
|
||||
FileLogAgeType int `json:"file_log_age_type,omitempty"`
|
||||
FileLogBackupEnabled bool `json:"file_log_backup_enabled,omitempty"`
|
||||
FileLogDeleteOld bool `json:"file_log_delete_old,omitempty"`
|
||||
FileLogEnabled bool `json:"file_log_enabled,omitempty"`
|
||||
FileLogMaxSize int `json:"file_log_max_size,omitempty"`
|
||||
FileLogPath string `json:"file_log_path,omitempty"`
|
||||
FilePoolSize int `json:"file_pool_size,omitempty"`
|
||||
HashingThreads int `json:"hashing_threads,omitempty"`
|
||||
I2PAddress string `json:"i2p_address,omitempty"`
|
||||
I2PEnabled bool `json:"i2p_enabled,omitempty"`
|
||||
I2PInboundLength int `json:"i2p_inbound_length,omitempty"`
|
||||
I2PInboundQuantity int `json:"i2p_inbound_quantity,omitempty"`
|
||||
I2PMixedMode bool `json:"i2p_mixed_mode,omitempty"`
|
||||
I2POutboundLength int `json:"i2p_outbound_length,omitempty"`
|
||||
I2POutboundQuantity int `json:"i2p_outbound_quantity,omitempty"`
|
||||
I2PPort int `json:"i2p_port,omitempty"`
|
||||
IdnSupportEnabled bool `json:"idn_support_enabled,omitempty"`
|
||||
IncompleteFilesExt bool `json:"incomplete_files_ext,omitempty"`
|
||||
IPFilterEnabled bool `json:"ip_filter_enabled,omitempty"`
|
||||
IPFilterPath string `json:"ip_filter_path,omitempty"`
|
||||
IPFilterTrackers bool `json:"ip_filter_trackers,omitempty"`
|
||||
LimitLanPeers bool `json:"limit_lan_peers,omitempty"`
|
||||
LimitTCPOverhead bool `json:"limit_tcp_overhead,omitempty"`
|
||||
LimitUtpRate bool `json:"limit_utp_rate,omitempty"`
|
||||
ListenPort int `json:"listen_port,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Lsd bool `json:"lsd,omitempty"`
|
||||
MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled,omitempty"`
|
||||
MailNotificationEmail string `json:"mail_notification_email,omitempty"`
|
||||
MailNotificationEnabled bool `json:"mail_notification_enabled,omitempty"`
|
||||
MailNotificationPassword string `json:"mail_notification_password,omitempty"`
|
||||
MailNotificationSender string `json:"mail_notification_sender,omitempty"`
|
||||
MailNotificationSMTP string `json:"mail_notification_smtp,omitempty"`
|
||||
MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled,omitempty"`
|
||||
MailNotificationUsername string `json:"mail_notification_username,omitempty"`
|
||||
MaxActiveCheckingTorrents int `json:"max_active_checking_torrents,omitempty"`
|
||||
MaxActiveDownloads int `json:"max_active_downloads,omitempty"`
|
||||
MaxActiveTorrents int `json:"max_active_torrents,omitempty"`
|
||||
MaxActiveUploads int `json:"max_active_uploads,omitempty"`
|
||||
MaxConcurrentHTTPAnnounces int `json:"max_concurrent_http_announces,omitempty"`
|
||||
MaxConnec int `json:"max_connec,omitempty"`
|
||||
MaxConnecPerTorrent int `json:"max_connec_per_torrent,omitempty"`
|
||||
MaxInactiveSeedingTime int `json:"max_inactive_seeding_time,omitempty"`
|
||||
MaxInactiveSeedingTimeEnabled bool `json:"max_inactive_seeding_time_enabled,omitempty"`
|
||||
MaxRatio int `json:"max_ratio,omitempty"`
|
||||
MaxRatioAct int `json:"max_ratio_act,omitempty"`
|
||||
MaxRatioEnabled bool `json:"max_ratio_enabled,omitempty"`
|
||||
MaxSeedingTime int `json:"max_seeding_time,omitempty"`
|
||||
MaxSeedingTimeEnabled bool `json:"max_seeding_time_enabled,omitempty"`
|
||||
MaxUploads int `json:"max_uploads,omitempty"`
|
||||
MaxUploadsPerTorrent int `json:"max_uploads_per_torrent,omitempty"`
|
||||
MemoryWorkingSetLimit int `json:"memory_working_set_limit,omitempty"`
|
||||
MergeTrackers bool `json:"merge_trackers,omitempty"`
|
||||
OutgoingPortsMax int `json:"outgoing_ports_max,omitempty"`
|
||||
OutgoingPortsMin int `json:"outgoing_ports_min,omitempty"`
|
||||
PeerTos int `json:"peer_tos,omitempty"`
|
||||
PeerTurnover int `json:"peer_turnover,omitempty"`
|
||||
PeerTurnoverCutoff int `json:"peer_turnover_cutoff,omitempty"`
|
||||
PeerTurnoverInterval int `json:"peer_turnover_interval,omitempty"`
|
||||
PerformanceWarning bool `json:"performance_warning,omitempty"`
|
||||
Pex bool `json:"pex,omitempty"`
|
||||
PreallocateAll bool `json:"preallocate_all,omitempty"`
|
||||
ProxyAuthEnabled bool `json:"proxy_auth_enabled,omitempty"`
|
||||
ProxyBittorrent bool `json:"proxy_bittorrent,omitempty"`
|
||||
ProxyHostnameLookup bool `json:"proxy_hostname_lookup,omitempty"`
|
||||
ProxyIP string `json:"proxy_ip,omitempty"`
|
||||
ProxyMisc bool `json:"proxy_misc,omitempty"`
|
||||
ProxyPassword string `json:"proxy_password,omitempty"`
|
||||
ProxyPeerConnections bool `json:"proxy_peer_connections,omitempty"`
|
||||
ProxyPort int `json:"proxy_port,omitempty"`
|
||||
ProxyRss bool `json:"proxy_rss,omitempty"`
|
||||
ProxyType string `json:"proxy_type,omitempty"`
|
||||
ProxyUsername string `json:"proxy_username,omitempty"`
|
||||
QueueingEnabled bool `json:"queueing_enabled,omitempty"`
|
||||
RandomPort bool `json:"random_port,omitempty"`
|
||||
ReannounceWhenAddressChanged bool `json:"reannounce_when_address_changed,omitempty"`
|
||||
RecheckCompletedTorrents bool `json:"recheck_completed_torrents,omitempty"`
|
||||
RefreshInterval int `json:"refresh_interval,omitempty"`
|
||||
RequestQueueSize int `json:"request_queue_size,omitempty"`
|
||||
ResolvePeerCountries bool `json:"resolve_peer_countries,omitempty"`
|
||||
ResumeDataStorageType string `json:"resume_data_storage_type,omitempty"`
|
||||
RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled,omitempty"`
|
||||
RssDownloadRepackProperEpisodes bool `json:"rss_download_repack_proper_episodes,omitempty"`
|
||||
RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed,omitempty"`
|
||||
RssProcessingEnabled bool `json:"rss_processing_enabled,omitempty"`
|
||||
RssRefreshInterval int `json:"rss_refresh_interval,omitempty"`
|
||||
RssSmartEpisodeFilters string `json:"rss_smart_episode_filters,omitempty"`
|
||||
SavePath string `json:"save_path,omitempty"`
|
||||
SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled,omitempty"`
|
||||
SaveResumeDataInterval int `json:"save_resume_data_interval,omitempty"`
|
||||
ScheduleFromHour int `json:"schedule_from_hour,omitempty"`
|
||||
ScheduleFromMin int `json:"schedule_from_min,omitempty"`
|
||||
ScheduleToHour int `json:"schedule_to_hour,omitempty"`
|
||||
ScheduleToMin int `json:"schedule_to_min,omitempty"`
|
||||
SchedulerDays int `json:"scheduler_days,omitempty"`
|
||||
SchedulerEnabled bool `json:"scheduler_enabled,omitempty"`
|
||||
SendBufferLowWatermark int `json:"send_buffer_low_watermark,omitempty"`
|
||||
SendBufferWatermark int `json:"send_buffer_watermark,omitempty"`
|
||||
SendBufferWatermarkFactor int `json:"send_buffer_watermark_factor,omitempty"`
|
||||
SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold,omitempty"`
|
||||
SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer,omitempty"`
|
||||
SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold,omitempty"`
|
||||
SocketBacklogSize int `json:"socket_backlog_size,omitempty"`
|
||||
SocketReceiveBufferSize int `json:"socket_receive_buffer_size,omitempty"`
|
||||
SocketSendBufferSize int `json:"socket_send_buffer_size,omitempty"`
|
||||
SsrfMitigation bool `json:"ssrf_mitigation,omitempty"`
|
||||
StartPausedEnabled bool `json:"start_paused_enabled,omitempty"`
|
||||
StopTrackerTimeout int `json:"stop_tracker_timeout,omitempty"`
|
||||
TempPath string `json:"temp_path,omitempty"`
|
||||
TempPathEnabled bool `json:"temp_path_enabled,omitempty"`
|
||||
TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled,omitempty"`
|
||||
TorrentContentLayout string `json:"torrent_content_layout,omitempty"`
|
||||
TorrentFileSizeLimit int `json:"torrent_file_size_limit,omitempty"`
|
||||
TorrentStopCondition string `json:"torrent_stop_condition,omitempty"`
|
||||
UpLimit int `json:"up_limit,omitempty"`
|
||||
UploadChokingAlgorithm int `json:"upload_choking_algorithm,omitempty"`
|
||||
UploadSlotsBehavior int `json:"upload_slots_behavior,omitempty"`
|
||||
Upnp bool `json:"upnp,omitempty"`
|
||||
UpnpLeaseDuration int `json:"upnp_lease_duration,omitempty"`
|
||||
UseCategoryPathsInManualMode bool `json:"use_category_paths_in_manual_mode,omitempty"`
|
||||
UseHTTPS bool `json:"use_https,omitempty"`
|
||||
UseSubcategories bool `json:"use_subcategories,omitempty"`
|
||||
UtpTCPMixedMode int `json:"utp_tcp_mixed_mode,omitempty"`
|
||||
ValidateHTTPSTrackerCertificate bool `json:"validate_https_tracker_certificate,omitempty"`
|
||||
WebUIAddress string `json:"web_ui_address,omitempty"`
|
||||
WebUIBanDuration int `json:"web_ui_ban_duration,omitempty"`
|
||||
WebUIClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled,omitempty"`
|
||||
WebUICsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled,omitempty"`
|
||||
WebUICustomHTTPHeaders string `json:"web_ui_custom_http_headers,omitempty"`
|
||||
WebUIDomainList string `json:"web_ui_domain_list,omitempty"`
|
||||
WebUIHostHeaderValidationEnabled bool `json:"web_ui_host_header_validation_enabled,omitempty"`
|
||||
WebUIHTTPSCertPath string `json:"web_ui_https_cert_path,omitempty"`
|
||||
WebUIHTTPSKeyPath string `json:"web_ui_https_key_path,omitempty"`
|
||||
WebUIMaxAuthFailCount int `json:"web_ui_max_auth_fail_count,omitempty"`
|
||||
WebUIPort int `json:"web_ui_port,omitempty"`
|
||||
WebUIReverseProxiesList string `json:"web_ui_reverse_proxies_list,omitempty"`
|
||||
WebUIReverseProxyEnabled bool `json:"web_ui_reverse_proxy_enabled,omitempty"`
|
||||
WebUISecureCookieEnabled bool `json:"web_ui_secure_cookie_enabled,omitempty"`
|
||||
WebUISessionTimeout int `json:"web_ui_session_timeout,omitempty"`
|
||||
WebUIUpnp bool `json:"web_ui_upnp,omitempty"`
|
||||
WebUIUseCustomHTTPHeadersEnabled bool `json:"web_ui_use_custom_http_headers_enabled,omitempty"`
|
||||
WebUIUsername string `json:"web_ui_username,omitempty"`
|
||||
}
|
||||
|
||||
func (c *client) Version(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.Version")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/version", c.config.Address)
|
||||
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("get version failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
||||
|
||||
func (c *client) WebApiVersion(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.WebApiVersion")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/webapiVersion", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("get version failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
||||
|
||||
func (c *client) BuildInfo(ctx context.Context) (*BuildInfo, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.BuildInfo")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/buildInfo", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get build info failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var build = new(BuildInfo)
|
||||
if err := json.Unmarshal(result.body, build); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return build, nil
|
||||
}
|
||||
|
||||
func (c *client) Shutdown(ctx context.Context) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.Shutdown")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/shutdown", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
method: http.MethodPost,
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("shutdown application failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetPreferences(ctx context.Context) (*Preferences, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.GetPreferences")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/preferences", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get preference failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var preferences = new(Preferences)
|
||||
if err := json.Unmarshal(result.body, preferences); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return preferences, nil
|
||||
}
|
||||
|
||||
func (c *client) SetPreferences(ctx context.Context, prefs *Preferences) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.SetPreferences")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/setPreferences", c.config.Address)
|
||||
data, err := json.Marshal(prefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var formData bytes.Buffer
|
||||
formData.Write([]byte("json="))
|
||||
formData.Write(data)
|
||||
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
method: http.MethodPost,
|
||||
url: apiUrl,
|
||||
contentType: ContentTypeFormUrlEncoded,
|
||||
body: &formData,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("set preference failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) DefaultSavePath(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Application.DefaultSavePath")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/app/defaultSavePath", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("get default save path failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
73
plugins/qbittorrent/pkg/qbittorrent/application_test.go
Normal file
73
plugins/qbittorrent/pkg/qbittorrent/application_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_Version(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
version, err := c.Application().Version(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(version)
|
||||
}
|
||||
|
||||
func TestClient_WebApiVersion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
version, err := c.Application().WebApiVersion(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(version)
|
||||
}
|
||||
|
||||
func TestClient_BuildInfo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
info, err := c.Application().BuildInfo(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("build: %+v", info)
|
||||
}
|
||||
|
||||
func TestClient_Shutdown(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := c.Application().Shutdown(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("shutting down")
|
||||
}
|
||||
|
||||
func TestClient_GetPreferences(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
prefs, err := c.Application().GetPreferences(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("prefs: %+v", prefs)
|
||||
}
|
||||
|
||||
func TestClient_SetPreferences(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
prefs, err := c.Application().GetPreferences(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
prefs.FileLogAge = 301
|
||||
if err := c.Application().SetPreferences(ctx, prefs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("success")
|
||||
}
|
||||
|
||||
func TestClient_DefaultSavePath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path, err := c.Application().DefaultSavePath(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("path: %s", path)
|
||||
}
|
91
plugins/qbittorrent/pkg/qbittorrent/authentication.go
Normal file
91
plugins/qbittorrent/pkg/qbittorrent/authentication.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Authentication interface {
|
||||
// Login cookie-based authentication, after calling NewClient, do not need to call Login again,
|
||||
// it is the default behavior
|
||||
Login(ctx context.Context) error
|
||||
// Logout deactivate cookies
|
||||
Logout(ctx context.Context) error
|
||||
}
|
||||
|
||||
func (c *client) Login(ctx context.Context) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Login")
|
||||
defer span.End()
|
||||
|
||||
if c.config.Username == "" || c.config.Password == "" {
|
||||
return errors.New("username or password is empty")
|
||||
}
|
||||
|
||||
formData := url.Values{}
|
||||
formData.Set("username", c.config.Username)
|
||||
formData.Set("password", c.config.Password)
|
||||
encodedFormData := formData.Encode()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/auth/login", c.config.Address)
|
||||
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
method: http.MethodPost,
|
||||
url: apiUrl,
|
||||
body: strings.NewReader(encodedFormData),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("login failed: " + string(result.body))
|
||||
}
|
||||
|
||||
if string(result.body) == "Fails." {
|
||||
return ErrAuthFailed
|
||||
}
|
||||
|
||||
if string(result.body) != "Ok." {
|
||||
return errors.New("login failed: " + string(result.body))
|
||||
}
|
||||
|
||||
if c.client.Jar == nil {
|
||||
c.client.Jar, err = cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
u, err := url.Parse(c.config.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.client.Jar.SetCookies(u, result.cookies)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) Logout(ctx context.Context) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Logout")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/auth/logout", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
method: http.MethodPost,
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("logout failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
24
plugins/qbittorrent/pkg/qbittorrent/authentication_test.go
Normal file
24
plugins/qbittorrent/pkg/qbittorrent/authentication_test.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_Login(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := c.Authentication().Login(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Logout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := c.Authentication().Login(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.Authentication().Logout(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
72
plugins/qbittorrent/pkg/qbittorrent/client.go
Normal file
72
plugins/qbittorrent/pkg/qbittorrent/client.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/server/pkg/qbittorrent")
|
||||
|
||||
// Client represents a qBittorrent client
|
||||
type Client interface {
|
||||
// Authentication auth qBittorrent client
|
||||
Authentication() Authentication
|
||||
// Application get qBittorrent application info
|
||||
Application() Application
|
||||
// Log get qBittorrent log
|
||||
Log() Log
|
||||
// Sync get qBittorrent events
|
||||
Sync() Sync
|
||||
// Transfer transfer manage
|
||||
Transfer() Transfer
|
||||
// Torrent manage for torrent
|
||||
Torrent() Torrent
|
||||
// Search api for search
|
||||
Search() Search
|
||||
// RSS api for rss
|
||||
RSS() RSS
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, cfg *Config) (Client, error) {
|
||||
var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func LoginClient(ctx context.Context, cfg *Config) (Client, error) {
|
||||
var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
|
||||
if err := c.Authentication().Login(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.RefreshCookie {
|
||||
go c.refreshCookie()
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// newClient creates and returns a new clientPool
|
||||
func newClient(maxIdle int, timeout time.Duration) *http.Client {
|
||||
if maxIdle == 0 {
|
||||
maxIdle = 128
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = time.Second * 3
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
MaxIdleConns: maxIdle,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
128
plugins/qbittorrent/pkg/qbittorrent/client_impl.go
Normal file
128
plugins/qbittorrent/pkg/qbittorrent/client_impl.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type responseResult struct {
|
||||
code int
|
||||
body []byte
|
||||
cookies []*http.Cookie
|
||||
}
|
||||
|
||||
type requestData struct {
|
||||
method string
|
||||
url string
|
||||
contentType string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
var _ Client = (*client)(nil)
|
||||
|
||||
type client struct {
|
||||
config *Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (c *client) Authentication() Authentication {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Application() Application {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Log() Log {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Sync() Sync {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Transfer() Transfer {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Torrent() Torrent {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) Search() Search {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *client) RSS() RSS {
|
||||
return c
|
||||
}
|
||||
|
||||
// doRequest send request
|
||||
func (c *client) doRequest(ctx context.Context, data *requestData) (*responseResult, error) {
|
||||
if data.method == "" {
|
||||
data.method = "GET"
|
||||
}
|
||||
if data.contentType == "" {
|
||||
data.contentType = ContentTypeFormUrlEncoded
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, data.method, data.url, data.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", data.contentType)
|
||||
for key, value := range c.config.CustomHeaders {
|
||||
request.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
readAll, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &responseResult{code: resp.StatusCode, body: readAll, cookies: resp.Cookies()}, nil
|
||||
}
|
||||
|
||||
func (c *client) cookies() (string, error) {
|
||||
if c.client.Jar == nil {
|
||||
return "", ErrNotLogin
|
||||
}
|
||||
u, err := url.Parse(c.config.Address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cookies := c.client.Jar.Cookies(u)
|
||||
if len(cookies) == 0 {
|
||||
return "", ErrNotLogin
|
||||
}
|
||||
var builder strings.Builder
|
||||
for _, cookie := range cookies {
|
||||
builder.WriteString(fmt.Sprintf("%s=%s; ", cookie.Name, cookie.Value))
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (c *client) refreshCookie() {
|
||||
ctx := context.Background()
|
||||
if c.config.RefreshIntervals == 0 {
|
||||
c.config.RefreshIntervals = time.Hour
|
||||
}
|
||||
var ticker = time.NewTicker(c.config.RefreshIntervals)
|
||||
for range ticker.C {
|
||||
if err := c.Authentication().Logout(ctx); err != nil {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
}
|
56
plugins/qbittorrent/pkg/qbittorrent/client_test.go
Normal file
56
plugins/qbittorrent/pkg/qbittorrent/client_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
c Client
|
||||
)
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
var err error
|
||||
c, err = LoginClient(ctx, &Config{
|
||||
Address: "http://192.168.3.33:38080",
|
||||
Username: "admin",
|
||||
Password: "J0710cz5",
|
||||
RefreshIntervals: time.Hour,
|
||||
ConnectionTimeout: time.Second * 3,
|
||||
CustomHeaders: map[string]string{
|
||||
//"Origin": "http://192.168.3.33:8080",
|
||||
//"Referer": "http://192.168.3.33:8080",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormEncoder(t *testing.T) {
|
||||
var option = LogOption{
|
||||
Normal: true,
|
||||
Info: true,
|
||||
Warning: false,
|
||||
Critical: false,
|
||||
LastKnownId: 0,
|
||||
}
|
||||
var form = url.Values{}
|
||||
err := encoder.Encode(option, form)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(form)
|
||||
}
|
||||
|
||||
func TestFormEncode(t *testing.T) {
|
||||
var form = url.Values{}
|
||||
form.Add("username", "admin hahaha")
|
||||
form.Add("password", "J0710c?//&z5")
|
||||
fe := form.Encode()
|
||||
t.Log(fe)
|
||||
}
|
10
plugins/qbittorrent/pkg/qbittorrent/common.go
Normal file
10
plugins/qbittorrent/pkg/qbittorrent/common.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package qbittorrent
|
||||
|
||||
import "github.com/gorilla/schema"
|
||||
|
||||
const (
|
||||
ContentTypeJSON = "application/json"
|
||||
ContentTypeFormUrlEncoded = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
var encoder = schema.NewEncoder()
|
25
plugins/qbittorrent/pkg/qbittorrent/config.go
Normal file
25
plugins/qbittorrent/pkg/qbittorrent/config.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package qbittorrent
|
||||
|
||||
import "time"
|
||||
|
||||
type Config struct {
|
||||
// Address qBittorrent endpoint
|
||||
Address string
|
||||
// Username used to access the WebUI
|
||||
Username string
|
||||
// Password used to access the WebUI
|
||||
Password string
|
||||
|
||||
// HTTP configuration
|
||||
|
||||
// CustomHeaders custom headers
|
||||
CustomHeaders map[string]string
|
||||
// ConnectionTimeout request timeout, default 3 seconds
|
||||
ConnectionTimeout time.Duration
|
||||
// ConnectionMaxIdles http client pool, default 128
|
||||
ConnectionMaxIdles int
|
||||
// RefreshCookie whether to automatically refresh cookies
|
||||
RefreshCookie bool
|
||||
// SessionTimeout interval for refreshing cookies, default 1 hour
|
||||
RefreshIntervals time.Duration
|
||||
}
|
8
plugins/qbittorrent/pkg/qbittorrent/error_code.go
Normal file
8
plugins/qbittorrent/pkg/qbittorrent/error_code.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package qbittorrent
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotLogin = errors.New("not login")
|
||||
ErrAuthFailed = errors.New("auth failed")
|
||||
)
|
95
plugins/qbittorrent/pkg/qbittorrent/log.go
Normal file
95
plugins/qbittorrent/pkg/qbittorrent/log.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LogOption struct {
|
||||
Normal bool `schema:"normal,omitempty"` // include normal messages
|
||||
Info bool `schema:"info,omitempty"` // include info messages
|
||||
Warning bool `schema:"warning,omitempty"` // include warning messages
|
||||
Critical bool `schema:"critical,omitempty"` // include critical messages
|
||||
LastKnownId int64 `schema:"last_known_id,omitempty"` // exclude messages with "message id" <= (default: last_known_id-1)
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Id int `json:"id,omitempty"` // id of the message or peer
|
||||
Timestamp int `json:"timestamp,omitempty"` // seconds since epoch
|
||||
Type int `json:"type,omitempty"` // type of the message, Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
|
||||
Message string `json:"message,omitempty"` // text of the message
|
||||
IP string `json:"ip"` // ip of the peer
|
||||
Blocked bool `json:"blocked,omitempty"` // whether the peer was blocked
|
||||
Reason string `json:"reason,omitempty"` // Reason of the block
|
||||
}
|
||||
|
||||
type Log interface {
|
||||
// GetLog get log
|
||||
GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, error)
|
||||
// GetPeerLog get peer log
|
||||
GetPeerLog(ctx context.Context, lastKnownId int) ([]*LogEntry, error)
|
||||
}
|
||||
|
||||
func (c *client) GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Log.GetLog")
|
||||
defer span.End()
|
||||
|
||||
var form = url.Values{}
|
||||
err := encoder.Encode(option, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/log/main?%s", c.config.Address, form.Encode())
|
||||
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
body: strings.NewReader(form.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get log failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var logs []*LogEntry
|
||||
if err := json.Unmarshal(result.body, &logs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (c *client) GetPeerLog(ctx context.Context, lastKnownId int) ([]*LogEntry, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Log.GetPeerLog")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/log/peers", c.config.Address)
|
||||
var form = url.Values{}
|
||||
form.Add("last_known_id", strconv.Itoa(lastKnownId))
|
||||
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
body: strings.NewReader(form.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get peer log failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var logs []*LogEntry
|
||||
if err := json.Unmarshal(result.body, &logs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
39
plugins/qbittorrent/pkg/qbittorrent/log_test.go
Normal file
39
plugins/qbittorrent/pkg/qbittorrent/log_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_GetLog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
entries, err := c.Log().GetLog(ctx, &LogOption{
|
||||
Normal: true,
|
||||
Info: true,
|
||||
Warning: true,
|
||||
Critical: true,
|
||||
LastKnownId: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetPeerLog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
entries, err := c.Log().GetPeerLog(ctx, -1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
359
plugins/qbittorrent/pkg/qbittorrent/rss.go
Normal file
359
plugins/qbittorrent/pkg/qbittorrent/rss.go
Normal file
|
@ -0,0 +1,359 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RSS interface {
|
||||
// AddFolder create new folder for rss, full path of added folder such as "The Pirate Bay\Top100"
|
||||
AddFolder(ctx context.Context, path string) error
|
||||
// AddFeed add feed
|
||||
AddFeed(ctx context.Context, option *RssAddFeedOption) error
|
||||
// RemoveItem remove folder or feed
|
||||
RemoveItem(ctx context.Context, path string) error
|
||||
// MoveItem move or rename folder or feed
|
||||
MoveItem(ctx context.Context, srcPath, destPath string) error
|
||||
// GetItems list all items, if withData is true, will return all data
|
||||
GetItems(ctx context.Context, withData bool) (map[string]interface{}, error)
|
||||
// MarkAsRead if articleId is provided only the article is marked as read otherwise the whole feed
|
||||
// is going to be marked as read.
|
||||
MarkAsRead(ctx context.Context, option *RssMarkAsReadOption) error
|
||||
// RefreshItem refresh folder or feed
|
||||
RefreshItem(ctx context.Context, itemPath string) error
|
||||
// SetAutoDownloadingRule set auto-downloading rule
|
||||
SetAutoDownloadingRule(ctx context.Context, ruleName string, ruleDef *RssAutoDownloadingRuleDef) error
|
||||
// RenameAutoDownloadingRule rename auto-downloading rule
|
||||
RenameAutoDownloadingRule(ctx context.Context, ruleName, newRuleName string) error
|
||||
// RemoveAutoDownloadingRule remove auto-downloading rule
|
||||
RemoveAutoDownloadingRule(ctx context.Context, ruleName string) error
|
||||
// GetAllAutoDownloadingRules get all auto-downloading rules
|
||||
GetAllAutoDownloadingRules(ctx context.Context) (map[string]*RssAutoDownloadingRuleDef, error)
|
||||
// GetAllArticlesMatchingRule get all articles matching a rule
|
||||
GetAllArticlesMatchingRule(ctx context.Context, ruleName string) (map[string][]string, error)
|
||||
}
|
||||
|
||||
type RssAddFeedOption struct {
|
||||
// URL feed of rss such as http://thepiratebay.org/rss//top100/200
|
||||
URL string `schema:"url"`
|
||||
// Folder full path of added folder, optional
|
||||
Folder string `schema:"path,omitempty"`
|
||||
}
|
||||
|
||||
type RssMarkAsReadOption struct {
|
||||
// ItemPath current full path of item
|
||||
ItemPath string `schema:"itemPath"`
|
||||
// ArticleId id of article, optional
|
||||
ArticleId string `schema:"articleId,omitempty"`
|
||||
}
|
||||
|
||||
type RssAutoDownloadingRuleDefTorrentParams struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
DownloadLimit int `json:"download_limit,omitempty"`
|
||||
DownloadPath int `json:"download_path,omitempty"`
|
||||
InactiveSeedingTimeLimit int `json:"inactive_seeding_time_limit,omitempty"`
|
||||
OperatingMode string `json:"operating_mode,omitempty"`
|
||||
RatioLimit int `json:"ratio_limit,omitempty"`
|
||||
SavePath string `json:"save_path,omitempty"`
|
||||
SeedingTimeLimit int `json:"seeding_time_limit,omitempty"`
|
||||
SkipChecking bool `json:"skip_checking,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
UploadLimit int `json:"upload_limit,omitempty"`
|
||||
Stopped bool `json:"stopped,omitempty"`
|
||||
UseAutoTMM bool `json:"use_auto_tmm,omitempty"`
|
||||
}
|
||||
|
||||
type RssAutoDownloadingRuleDef struct {
|
||||
AddPaused bool `json:"addPaused,omitempty"`
|
||||
AffectedFeeds []string `json:"affectedFeeds,omitempty"`
|
||||
AssignedCategory string `json:"assignedCategory,omitempty"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
EpisodeFilter string `json:"episodeFilter,omitempty"`
|
||||
IgnoreDays int `json:"ignoreDays,omitempty"`
|
||||
LastMatch string `json:"lastMatch,omitempty"`
|
||||
MustContain string `json:"mustContain,omitempty"`
|
||||
MustNotContain string `json:"mustNotContain,omitempty"`
|
||||
PreviouslyMatchedEpisodes []string `json:"previouslyMatchedEpisodes,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
SavePath string `json:"savePath,omitempty"`
|
||||
SmartFilter bool `json:"smartFilter,omitempty"`
|
||||
TorrentParams *RssAutoDownloadingRuleDefTorrentParams `json:"torrentParams,omitempty"`
|
||||
UseRegex bool `json:"useRegex,omitempty"`
|
||||
}
|
||||
|
||||
func (c *client) AddFolder(ctx context.Context, path string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFolder")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("path", path)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/addFolder", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("add rss folder failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) AddFeed(ctx context.Context, opt *RssAddFeedOption) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFeed")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
err := encoder.Encode(opt, formData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/addFolder", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("add rss feed failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) RemoveItem(ctx context.Context, path string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveItem")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("path", path)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeItem", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("remove rss item failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) MoveItem(ctx context.Context, srcPath, destPath string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.MoveItem")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("itemPath", srcPath)
|
||||
formData.Add("destPath", destPath)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/moveItem", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("move rss item failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetItems(ctx context.Context, withData bool) (map[string]interface{}, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetItems")
|
||||
defer span.End()
|
||||
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/items?withData=%t", c.config.Address, withData)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodGet,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get rss items failed: " + string(result.body))
|
||||
}
|
||||
var data = make(map[string]interface{})
|
||||
err = json.Unmarshal(result.body, &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (c *client) MarkAsRead(ctx context.Context, opt *RssMarkAsReadOption) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.MarkAsRead")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
err := encoder.Encode(opt, formData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/markAsRead", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("mark as read rss item failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) RefreshItem(ctx context.Context, itemPath string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RefreshItem")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("itemPath", itemPath)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/refreshItem", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("refresh rss item failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) SetAutoDownloadingRule(ctx context.Context, ruleName string, ruleDef *RssAutoDownloadingRuleDef) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.SetAutoDownloadingRule")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("ruleName", ruleName)
|
||||
ruleDefBytes, err := json.Marshal(ruleDef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formData.Add("ruleDef", string(ruleDefBytes))
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/setRule", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("set auto downloading rule failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) RenameAutoDownloadingRule(ctx context.Context, ruleName, newRuleName string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RenameAutoDownloadingRule")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("ruleName", ruleName)
|
||||
formData.Add("newRuleName", newRuleName)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/renameRule", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("rename auto downloading rule failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) RemoveAutoDownloadingRule(ctx context.Context, ruleName string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveAutoDownloadingRule")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("ruleName", ruleName)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeRule", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(formData.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("remove auto downloading rule failed: " + string(result.body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetAllAutoDownloadingRules(ctx context.Context) (map[string]*RssAutoDownloadingRuleDef, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllAutoDownloadingRules")
|
||||
defer span.End()
|
||||
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get rss rules failed: " + string(result.body))
|
||||
}
|
||||
var data = make(map[string]*RssAutoDownloadingRuleDef)
|
||||
err = json.Unmarshal(result.body, &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (c *client) GetAllArticlesMatchingRule(ctx context.Context, ruleName string) (map[string][]string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllArticlesMatchingRule")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("ruleName", ruleName)
|
||||
var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles?%s", c.config.Address, formData.Encode())
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get rss rule match articles failed: " + string(result.body))
|
||||
}
|
||||
var data = make(map[string][]string)
|
||||
err = json.Unmarshal(result.body, &data)
|
||||
return data, err
|
||||
}
|
64
plugins/qbittorrent/pkg/qbittorrent/search.go
Normal file
64
plugins/qbittorrent/pkg/qbittorrent/search.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package qbittorrent
|
||||
|
||||
type Search interface {
|
||||
Start()
|
||||
Stop()
|
||||
Status()
|
||||
Results()
|
||||
Delete()
|
||||
Plugins()
|
||||
InstallPlugins()
|
||||
UninstallPlugins()
|
||||
EnableSearchPlugins()
|
||||
UpdateSearchPlugins()
|
||||
}
|
||||
|
||||
func (c *client) Start() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) Stop() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) Status() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) Results() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) Delete() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) Plugins() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) InstallPlugins() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) UninstallPlugins() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) EnableSearchPlugins() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c *client) UpdateSearchPlugins() {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
127
plugins/qbittorrent/pkg/qbittorrent/sync.go
Normal file
127
plugins/qbittorrent/pkg/qbittorrent/sync.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Sync interface {
|
||||
// MainData get sync main data, rid is Response ID. if not provided, will be assumed.
|
||||
// if the given is different from the one of last server reply, will be (see the server reply details for more info)
|
||||
MainData(ctx context.Context, rid int) (*SyncMainData, error)
|
||||
// TorrentPeersData get sync torrent peer data, hash is torrent hash, rid is response id
|
||||
TorrentPeersData(ctx context.Context, hash string, rid int) (*SyncTorrentPeers, error)
|
||||
}
|
||||
|
||||
type SyncMainData struct {
|
||||
Rid int `json:"rid,omitempty"`
|
||||
FullUpdate bool `json:"full_update,omitempty"`
|
||||
ServerState ServerState `json:"server_state,omitempty"`
|
||||
Torrents map[string]SyncTorrentInfo `json:"torrents,omitempty"`
|
||||
}
|
||||
|
||||
type ServerState struct {
|
||||
AllTimeDl int64 `json:"alltime_dl,omitempty"`
|
||||
AllTimeUl int64 `json:"alltime_ul,omitempty"`
|
||||
AverageTimeQueue int `json:"average_time_queue,omitempty"`
|
||||
DlInfoData int64 `json:"dl_info_data,omitempty"`
|
||||
DlInfoSpeed int `json:"dl_info_speed,omitempty"`
|
||||
QueuedIoJobs int `json:"queued_io_jobs,omitempty"`
|
||||
TotalBuffersSize int `json:"total_buffers_size,omitempty"`
|
||||
UpInfoData int64 `json:"up_info_data,omitempty"`
|
||||
UpInfoSpeed int `json:"up_info_speed,omitempty"`
|
||||
WriteCacheOverload string `json:"write_cache_overload,omitempty"`
|
||||
}
|
||||
|
||||
type SyncTorrentInfo struct {
|
||||
AmountLeft int64 `json:"amount_left,omitempty"`
|
||||
Completed int `json:"completed,omitempty"`
|
||||
DlSpeed int `json:"dlspeed,omitempty"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
DownloadedSession int `json:"downloaded_session,omitempty"`
|
||||
Eta int `json:"eta,omitempty"`
|
||||
Progress float64 `json:"progress,omitempty"`
|
||||
SeenComplete int `json:"seen_complete,omitempty"`
|
||||
TimeActive int `json:"time_active,omitempty"`
|
||||
}
|
||||
|
||||
type SyncTorrentPeers struct {
|
||||
Rid int `json:"rid,omitempty"`
|
||||
FullUpdate bool `json:"full_update,omitempty"`
|
||||
ShowFlags bool `json:"show_flags,omitempty"`
|
||||
Peers map[string]SyncTorrentPeer `json:"peers,omitempty"`
|
||||
}
|
||||
|
||||
type SyncTorrentPeer struct {
|
||||
Client string `json:"client,omitempty"`
|
||||
Connection string `json:"connection,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
DlSpeed int `json:"dl_speed,omitempty"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
Files string `json:"files,omitempty"`
|
||||
Flags string `json:"flags,omitempty"`
|
||||
FlagsDesc string `json:"flags_desc,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
PeerIDClient string `json:"peer_id_client,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Progress float64 `json:"progress,omitempty"`
|
||||
Relevance float64 `json:"relevance,omitempty"`
|
||||
UpSpeed int `json:"up_speed,omitempty"`
|
||||
Uploaded int `json:"uploaded,omitempty"`
|
||||
}
|
||||
|
||||
func (c *client) MainData(ctx context.Context, rid int) (*SyncMainData, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Sync.MainData")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/sync/maindata?rid=%d", c.config.Address, rid)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get main data failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var mainData = new(SyncMainData)
|
||||
if err := json.Unmarshal(result.body, mainData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainData, nil
|
||||
}
|
||||
|
||||
func (c *client) TorrentPeersData(ctx context.Context, hash string, rid int) (*SyncTorrentPeers, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Sync.TorrentPeersData")
|
||||
defer span.End()
|
||||
|
||||
var formData = url.Values{}
|
||||
formData.Add("hash", hash)
|
||||
formData.Add("rid", strconv.Itoa(rid))
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/sync/torrentPeers?%s", c.config.Address, formData.Encode())
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get torrent peers data failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var mainData = new(SyncTorrentPeers)
|
||||
if err := json.Unmarshal(result.body, mainData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mainData, nil
|
||||
}
|
37
plugins/qbittorrent/pkg/qbittorrent/sync_test.go
Normal file
37
plugins/qbittorrent/pkg/qbittorrent/sync_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestClient_MainData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
syncMainData, err := c.Sync().MainData(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("sync main data: %+v", syncMainData)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
syncMainData, err = c.Sync().MainData(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("sync main data: %+v", syncMainData)
|
||||
}
|
||||
|
||||
func TestClient_TorrentPeersData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
peersData, err := c.Sync().TorrentPeersData(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(peersData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
1530
plugins/qbittorrent/pkg/qbittorrent/torrent.go
Normal file
1530
plugins/qbittorrent/pkg/qbittorrent/torrent.go
Normal file
File diff suppressed because it is too large
Load diff
299
plugins/qbittorrent/pkg/qbittorrent/torrent_test.go
Normal file
299
plugins/qbittorrent/pkg/qbittorrent/torrent_test.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_GetTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
torrents, err := c.Torrent().GetTorrents(ctx, &TorrentOption{
|
||||
Filter: "",
|
||||
Category: "movies",
|
||||
Tag: "hdtime",
|
||||
Sort: "",
|
||||
Reverse: false,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
Hashes: nil,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(torrents)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetProperties(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
properties, err := c.Torrent().GetProperties(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(properties)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetTrackers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
trackers, err := c.Torrent().GetTrackers(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(trackers)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetWebSeeds(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
webSeeds, err := c.Torrent().GetWebSeeds(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(webSeeds)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetContents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
contents, err := c.Torrent().GetContents(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bytes, err := json.Marshal(contents)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(bytes))
|
||||
}
|
||||
|
||||
func TestClient_GetPiecesStates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
states, err := c.Torrent().GetPiecesStates(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(states)
|
||||
}
|
||||
|
||||
func TestClient_GetPiecesHashes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
hashes, err := c.Torrent().GetPiecesHashes(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(hashes)
|
||||
}
|
||||
|
||||
func TestClient_PauseTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().PauseTorrents(ctx, []string{"202382999be6a4fab395cd9c2c9d294177587904"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent paused")
|
||||
}
|
||||
|
||||
func TestClient_ResumeTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().ResumeTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent resumed")
|
||||
}
|
||||
|
||||
func TestClient_DeleteTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().DeleteTorrents(ctx, []string{"202382999be6a4fab395cd9c2c9d294177587904"}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent deleted")
|
||||
}
|
||||
|
||||
func TestClient_RecheckTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().RecheckTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent rechecked")
|
||||
}
|
||||
|
||||
func TestClient_ReAnnounceTorrents(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().ReAnnounceTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent reannounceed")
|
||||
}
|
||||
|
||||
func TestClient_AddNewTorrent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fileContent, err := os.ReadFile("C:\\Users\\xuthu\\Downloads\\bbbbb.torrent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = c.Torrent().AddNewTorrent(ctx, &TorrentAddOption{
|
||||
Torrents: []*TorrentAddFileMetadata{
|
||||
{
|
||||
//Filename: "ttttt.torrent",
|
||||
Data: fileContent,
|
||||
},
|
||||
},
|
||||
Category: "movies",
|
||||
Tags: []string{"d", "e", "f"},
|
||||
SkipChecking: false,
|
||||
Paused: false,
|
||||
RootFolder: false,
|
||||
Rename: "",
|
||||
UpLimit: 0,
|
||||
DlLimit: 0,
|
||||
RatioLimit: 0,
|
||||
SeedingTimeLimit: 0,
|
||||
AutoTMM: false,
|
||||
SequentialDownload: "",
|
||||
FirstLastPiecePrio: "",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent added")
|
||||
}
|
||||
|
||||
func TestClient_AddTrackers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().AddTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", []string{"https://hddtime.org/announce"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent trackers added")
|
||||
}
|
||||
|
||||
func TestClient_EditTrackers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().EditTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", "https://hddtime.org/announce", "https://hdctime.org/announce")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent trackers edited")
|
||||
}
|
||||
|
||||
func TestClient_RemoveTrackers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().RemoveTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", []string{"https://hdctime.org/announce"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent trackers removed")
|
||||
}
|
||||
|
||||
func TestClient_AddPeers(t *testing.T) {
|
||||
// todo no test
|
||||
//c.Torrent().AddPeers([]string{"ca4523a3db9c6c3a13d7d7f3a545f97b75083032"}, []string{"10.0.0.1:38080"})
|
||||
}
|
||||
|
||||
func TestClient_IncreasePriority(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().IncreasePriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent priority increased")
|
||||
}
|
||||
|
||||
func TestClient_DecreasePriority(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().DecreasePriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent priority decreased")
|
||||
}
|
||||
|
||||
func TestClient_MaxPriority(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().MaxPriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent priority maxed")
|
||||
}
|
||||
|
||||
func TestClient_MinPriority(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().MinPriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent priority mined")
|
||||
}
|
||||
|
||||
func TestClient_SetFilePriority(t *testing.T) {
|
||||
// todo no test
|
||||
}
|
||||
|
||||
func TestClient_GetDownloadLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
downloadLimit, err := c.Torrent().GetDownloadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent download limit", downloadLimit)
|
||||
}
|
||||
|
||||
func TestClient_SetDownloadLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().SetDownloadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent download limit setted")
|
||||
}
|
||||
|
||||
func TestClient_SetShareLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().SetShareLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, -2, -2, -2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent share limit setted")
|
||||
}
|
||||
|
||||
func TestClient_GetUploadLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
limit, err := c.Torrent().GetUploadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent upload limit", limit)
|
||||
}
|
||||
|
||||
func TestClient_SetUploadLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := c.Torrent().SetUploadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("torrent upload limit setted")
|
||||
}
|
||||
|
||||
func TestClient_SetLocation(t *testing.T) {
|
||||
// todo test
|
||||
}
|
209
plugins/qbittorrent/pkg/qbittorrent/transfer.go
Normal file
209
plugins/qbittorrent/pkg/qbittorrent/transfer.go
Normal file
|
@ -0,0 +1,209 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TransferStatusBar struct {
|
||||
ConnectionStatus string `json:"connection_status,omitempty"`
|
||||
DhtNodes int `json:"dht_nodes,omitempty"`
|
||||
DlInfoData int64 `json:"dl_info_data,omitempty"`
|
||||
DlInfoSpeed int `json:"dl_info_speed,omitempty"`
|
||||
DlRateLimit int `json:"dl_rate_limit,omitempty"`
|
||||
UpInfoData int `json:"up_info_data,omitempty"`
|
||||
UpInfoSpeed int `json:"up_info_speed,omitempty"`
|
||||
UpRateLimit int `json:"up_rate_limit,omitempty"`
|
||||
Queueing bool `json:"queueing,omitempty"`
|
||||
UseAltSpeedLimits bool `json:"use_alt_speed_limits,omitempty"`
|
||||
RefreshInterval int `json:"refresh_interval,omitempty"`
|
||||
}
|
||||
|
||||
type Transfer interface {
|
||||
// GlobalStatusBar usually see in qBittorrent status bar
|
||||
GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error)
|
||||
// BanPeers the peer to ban, or multiple peers separated by a pipe.
|
||||
// each peer is host:port
|
||||
BanPeers(ctx context.Context, peers []string) error
|
||||
// GetSpeedLimitsMode get alternative speed limits state
|
||||
GetSpeedLimitsMode(ctx context.Context) (string, error)
|
||||
// ToggleSpeedLimitsMode toggle alternative speed limits
|
||||
ToggleSpeedLimitsMode(ctx context.Context) error
|
||||
// GetGlobalUploadLimit get global upload limit, the response is the value of current global download speed
|
||||
// limit in bytes/second; this value will be zero if no limit is applied.
|
||||
GetGlobalUploadLimit(ctx context.Context) (string, error)
|
||||
// SetGlobalUploadLimit set global upload limit, set in bytes/second
|
||||
SetGlobalUploadLimit(ctx context.Context, limit int) error
|
||||
// GetGlobalDownloadLimit get global download limit, the response is the value of current global download speed
|
||||
// limit in bytes/second; this value will be zero if no limit is applied.
|
||||
GetGlobalDownloadLimit(ctx context.Context) (string, error)
|
||||
// SetGlobalDownloadLimit set global download limit, set in bytes/second
|
||||
SetGlobalDownloadLimit(ctx context.Context, limit int) error
|
||||
}
|
||||
|
||||
func (c *client) GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GlobalStatusBar")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/info", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return nil, errors.New("get global transfer status bar failed: " + string(result.body))
|
||||
}
|
||||
|
||||
var data = new(TransferStatusBar)
|
||||
if err := json.Unmarshal(result.body, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *client) BanPeers(ctx context.Context, peers []string) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.BanPeers")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/banPeers", c.config.Address)
|
||||
var form = url.Values{}
|
||||
form.Add("peers", strings.Join(peers, "|"))
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
body: strings.NewReader(form.Encode()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("ban peers failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetSpeedLimitsMode(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetSpeedLimitsMode")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/speedLimitsMode", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("ban peers failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
||||
|
||||
func (c *client) ToggleSpeedLimitsMode(ctx context.Context) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.ToggleSpeedLimitsMode")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/toggleSpeedLimitsMode", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
method: http.MethodPost,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("ban peers failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetGlobalUploadLimit(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalUploadLimit")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/uploadLimit", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("get global upload limit failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
||||
|
||||
func (c *client) SetGlobalUploadLimit(ctx context.Context, limit int) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalUploadLimit")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/setUploadLimit?limit=%d", c.config.Address, limit)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("set global upload limit failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) GetGlobalDownloadLimit(ctx context.Context) (string, error) {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalDownloadLimit")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/downloadLimit", c.config.Address)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return "", errors.New("get global download limit failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return string(result.body), nil
|
||||
}
|
||||
|
||||
func (c *client) SetGlobalDownloadLimit(ctx context.Context, limit int) error {
|
||||
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalDownloadLimit")
|
||||
defer span.End()
|
||||
|
||||
apiUrl := fmt.Sprintf("%s/api/v2/transfer/setDownloadLimit?limit=%d", c.config.Address, limit)
|
||||
result, err := c.doRequest(ctx, &requestData{
|
||||
url: apiUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.code != 200 {
|
||||
return errors.New("set global download limit failed: " + string(result.body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
13
plugins/qbittorrent/plugin/main.go
Normal file
13
plugins/qbittorrent/plugin/main.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent"
|
||||
"git.kmsign.ru/royalcat/tstor/server/src/daemon"
|
||||
)
|
||||
|
||||
func main() {
|
||||
}
|
||||
|
||||
var DaemonName = qbittorrent.DaemonName
|
||||
|
||||
var NewDaemon daemon.DaemonConstructor = qbittorrent.NewDaemon
|
Loading…
Add table
Add a link
Reference in a new issue