node build fixed
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package qbittorrent_application
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
qbittorrent_model "seanime/internal/torrent_clients/qbittorrent/model"
|
||||
qbittorrent_util "seanime/internal/torrent_clients/qbittorrent/util"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) GetAppVersion() (string, error) {
|
||||
var res string
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/version", nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetAPIVersion() (string, error) {
|
||||
var res string
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/webapiVersion", nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetBuildInfo() (*qbittorrent_model.BuildInfo, error) {
|
||||
var res qbittorrent_model.BuildInfo
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/buildInfo", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetAppPreferences() (*qbittorrent_model.Preferences, error) {
|
||||
var res qbittorrent_model.Preferences
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/preferences", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) SetAppPreferences(p *qbittorrent_model.Preferences) error {
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/setPreferences", p); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetDefaultSavePath() (string, error) {
|
||||
var res string
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/defaultSavePath", nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
162
seanime-2.9.10/internal/torrent_clients/qbittorrent/client.go
Normal file
162
seanime-2.9.10/internal/torrent_clients/qbittorrent/client.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"seanime/internal/torrent_clients/qbittorrent/application"
|
||||
"seanime/internal/torrent_clients/qbittorrent/log"
|
||||
"seanime/internal/torrent_clients/qbittorrent/rss"
|
||||
"seanime/internal/torrent_clients/qbittorrent/search"
|
||||
"seanime/internal/torrent_clients/qbittorrent/sync"
|
||||
"seanime/internal/torrent_clients/qbittorrent/torrent"
|
||||
"seanime/internal/torrent_clients/qbittorrent/transfer"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
logger *zerolog.Logger
|
||||
client *http.Client
|
||||
Username string
|
||||
Password string
|
||||
Port int
|
||||
Host string
|
||||
Path string
|
||||
DisableBinaryUse bool
|
||||
Tags string
|
||||
Application qbittorrent_application.Client
|
||||
Log qbittorrent_log.Client
|
||||
RSS qbittorrent_rss.Client
|
||||
Search qbittorrent_search.Client
|
||||
Sync qbittorrent_sync.Client
|
||||
Torrent qbittorrent_torrent.Client
|
||||
Transfer qbittorrent_transfer.Client
|
||||
}
|
||||
|
||||
type NewClientOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
Username string
|
||||
Password string
|
||||
Port int
|
||||
Host string
|
||||
Path string
|
||||
DisableBinaryUse bool
|
||||
Tags string
|
||||
}
|
||||
|
||||
func NewClient(opts *NewClientOptions) *Client {
|
||||
baseURL := fmt.Sprintf("http://%s:%d/api/v2", opts.Host, opts.Port)
|
||||
|
||||
if strings.HasPrefix(opts.Host, "https://") {
|
||||
opts.Host = strings.TrimPrefix(opts.Host, "https://")
|
||||
baseURL = fmt.Sprintf("https://%s:%d/api/v2", opts.Host, opts.Port)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
logger: opts.Logger,
|
||||
client: client,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
Port: opts.Port,
|
||||
Path: opts.Path,
|
||||
DisableBinaryUse: opts.DisableBinaryUse,
|
||||
Host: opts.Host,
|
||||
Tags: opts.Tags,
|
||||
Application: qbittorrent_application.Client{
|
||||
BaseUrl: baseURL + "/app",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
Log: qbittorrent_log.Client{
|
||||
BaseUrl: baseURL + "/log",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
RSS: qbittorrent_rss.Client{
|
||||
BaseUrl: baseURL + "/rss",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
Search: qbittorrent_search.Client{
|
||||
BaseUrl: baseURL + "/search",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
Sync: qbittorrent_sync.Client{
|
||||
BaseUrl: baseURL + "/sync",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
Torrent: qbittorrent_torrent.Client{
|
||||
BaseUrl: baseURL + "/torrents",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
Transfer: qbittorrent_transfer.Client{
|
||||
BaseUrl: baseURL + "/transfer",
|
||||
Client: client,
|
||||
Logger: opts.Logger,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Login() error {
|
||||
endpoint := c.baseURL + "/auth/login"
|
||||
data := url.Values{}
|
||||
data.Add("username", c.Username)
|
||||
data.Add("password", c.Password)
|
||||
request, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("content-type", "application/x-www-form-urlencoded")
|
||||
resp, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
c.logger.Err(err).Msg("failed to close login response body")
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
if len(resp.Cookies()) < 1 {
|
||||
return fmt.Errorf("no cookies in login response")
|
||||
}
|
||||
apiURL, err := url.Parse(c.baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jar.SetCookies(apiURL, []*http.Cookie{resp.Cookies()[0]})
|
||||
c.client.Jar = jar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Logout() error {
|
||||
endpoint := c.baseURL + "/auth/logout"
|
||||
request, err := http.NewRequest("POST", endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetList(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
|
||||
client := NewClient(&NewClientOptions{
|
||||
Logger: util.NewLogger(),
|
||||
Username: test_utils.ConfigData.Provider.QbittorrentUsername,
|
||||
Password: test_utils.ConfigData.Provider.QbittorrentPassword,
|
||||
Port: test_utils.ConfigData.Provider.QbittorrentPort,
|
||||
Host: test_utils.ConfigData.Provider.QbittorrentHost,
|
||||
Path: test_utils.ConfigData.Provider.QbittorrentPath,
|
||||
})
|
||||
|
||||
res, err := client.Torrent.GetList(&qbittorrent_model.GetTorrentListOptions{
|
||||
Filter: "",
|
||||
Category: nil,
|
||||
Sort: "",
|
||||
Reverse: false,
|
||||
Limit: 0,
|
||||
Offset: 0,
|
||||
Hashes: "",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, torrent := range res {
|
||||
t.Logf("%+v", torrent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetMainDataList(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
|
||||
client := NewClient(&NewClientOptions{
|
||||
Logger: util.NewLogger(),
|
||||
Username: test_utils.ConfigData.Provider.QbittorrentUsername,
|
||||
Password: test_utils.ConfigData.Provider.QbittorrentPassword,
|
||||
Port: test_utils.ConfigData.Provider.QbittorrentPort,
|
||||
Host: test_utils.ConfigData.Provider.QbittorrentHost,
|
||||
Path: test_utils.ConfigData.Provider.QbittorrentPath,
|
||||
})
|
||||
|
||||
res, err := client.Sync.GetMainData(0)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, torrent := range res.Torrents {
|
||||
t.Logf("%+v", torrent)
|
||||
}
|
||||
|
||||
res2, err := client.Sync.GetMainData(res.RID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, len(res2.Torrents))
|
||||
|
||||
for _, torrent := range res2.Torrents {
|
||||
t.Logf("%+v", torrent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetActiveTorrents(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
|
||||
client := NewClient(&NewClientOptions{
|
||||
Logger: util.NewLogger(),
|
||||
Username: test_utils.ConfigData.Provider.QbittorrentUsername,
|
||||
Password: test_utils.ConfigData.Provider.QbittorrentPassword,
|
||||
Port: test_utils.ConfigData.Provider.QbittorrentPort,
|
||||
Host: test_utils.ConfigData.Provider.QbittorrentHost,
|
||||
Path: test_utils.ConfigData.Provider.QbittorrentPath,
|
||||
})
|
||||
|
||||
res, err := client.Torrent.GetList(&qbittorrent_model.GetTorrentListOptions{
|
||||
Filter: "active",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, torrent := range res {
|
||||
t.Logf("%+v", torrent.Name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package qbittorrent_log
|
||||
|
||||
import (
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/torrent_clients/qbittorrent/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) GetLog(options *qbittorrent_model.GetLogOptions) ([]*qbittorrent_model.LogEntry, error) {
|
||||
endpoint := c.BaseUrl + "/main"
|
||||
if options != nil {
|
||||
params, err := query.Values(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint += "?" + params.Encode()
|
||||
}
|
||||
var res []*qbittorrent_model.LogEntry
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetPeerLog(lastKnownID int) ([]*qbittorrent_model.PeerLogEntry, error) {
|
||||
params := url.Values{}
|
||||
params.Add("last_known_id", strconv.Itoa(lastKnownID))
|
||||
endpoint := c.BaseUrl + "/peers?" + params.Encode()
|
||||
var res []*qbittorrent_model.PeerLogEntry
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type AddTorrentsOptions struct {
|
||||
// Download folder
|
||||
Savepath string `json:"savepath,omitempty"`
|
||||
// Cookie sent to download the .torrent file
|
||||
Cookie string `json:"cookie,omitempty"`
|
||||
// Category for the torrent
|
||||
Category string `json:"category,omitempty"`
|
||||
// Skip hash checking.
|
||||
SkipChecking bool `json:"skip_checking,omitempty"`
|
||||
// Add torrents in the paused state.
|
||||
Paused string `json:"paused,omitempty"`
|
||||
// Create the root folder. Possible values are true, false, unset (default)
|
||||
RootFolder string `json:"root_folder,omitempty"`
|
||||
// Rename torrent
|
||||
Rename string `json:"rename,omitempty"`
|
||||
// Set torrent upload speed limit. Unit in bytes/second
|
||||
UpLimit int `json:"upLimit,omitempty"`
|
||||
// Set torrent download speed limit. Unit in bytes/second
|
||||
DlLimit int `json:"dlLimit,omitempty"`
|
||||
// Whether Automatic Torrent Management should be used
|
||||
UseAutoTMM bool `json:"useAutoTMM,omitempty"`
|
||||
// Enable sequential download. Possible values are true, false (default)
|
||||
SequentialDownload bool `json:"sequentialDownload,omitempty"`
|
||||
// Prioritize download first last piece. Possible values are true, false (default)
|
||||
FirstLastPiecePrio bool `json:"firstLastPiecePrio,omitempty"`
|
||||
// Tags for the torrent, split by ','
|
||||
Tags string `json:"tags,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type BuildInfo struct {
|
||||
QT string `json:"qt"`
|
||||
LibTorrent string `json:"libtorrent"`
|
||||
Boost string `json:"boost"`
|
||||
OpenSSL string `json:"openssl"`
|
||||
Bitness string `json:"bitness"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type Category struct {
|
||||
Name string `json:"name"`
|
||||
SavePath string `json:"savePath"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type GetLogOptions struct {
|
||||
Normal bool `url:"normal"`
|
||||
Info bool `url:"info"`
|
||||
Warning bool `url:"warning"`
|
||||
Critical bool `url:"critical"`
|
||||
LastKnownID int `url:"lastKnownId"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type GetTorrentListOptions struct {
|
||||
Filter TorrentListFilter `url:"filter,omitempty"`
|
||||
Category *string `url:"category,omitempty"`
|
||||
Sort string `url:"sort,omitempty"`
|
||||
Reverse bool `url:"reverse,omitempty"`
|
||||
Limit int `url:"limit,omitempty"`
|
||||
Offset int `url:"offset,omitempty"`
|
||||
Hashes string `url:"hashes,omitempty"`
|
||||
}
|
||||
|
||||
type TorrentListFilter string
|
||||
|
||||
const (
|
||||
FilterAll TorrentListFilter = "all"
|
||||
FilterDownloading = "downloading"
|
||||
FilterCompleted = "completed"
|
||||
FilterPaused = "paused"
|
||||
FilterActive = "active"
|
||||
FilterInactive = "inactive"
|
||||
FilterResumed = "resumed"
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
package qbittorrent_model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
ID int `json:"id"`
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Type LogType `json:"type"`
|
||||
}
|
||||
|
||||
func (l *LogEntry) UnmarshalJSON(data []byte) error {
|
||||
var raw rawLogEntry
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
t := time.Unix(0, int64(raw.Timestamp)*int64(time.Millisecond))
|
||||
*l = LogEntry{
|
||||
ID: raw.ID,
|
||||
Message: raw.Message,
|
||||
Timestamp: t,
|
||||
Type: raw.Type,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LogType int
|
||||
|
||||
const (
|
||||
TypeNormal LogType = iota << 1
|
||||
TypeInfo
|
||||
TypeWarning
|
||||
TypeCritical
|
||||
)
|
||||
|
||||
type rawLogEntry struct {
|
||||
ID int `json:"id"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Type LogType `json:"type"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type Peer struct {
|
||||
Client string `json:"client"`
|
||||
Connection string `json:"connection"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code"`
|
||||
DLSpeed int `json:"dlSpeed"`
|
||||
Downloaded int `json:"downloaded"`
|
||||
Files string `json:"files"`
|
||||
Flags string `json:"flags"`
|
||||
FlagsDescription string `json:"flags_desc"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Progress float64 `json:"progress"`
|
||||
Relevance int `json:"relevance"`
|
||||
ULSpeed int `json:"up_speed"`
|
||||
Uploaded int `json:"uploaded"`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package qbittorrent_model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PeerLogEntry struct {
|
||||
ID int `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Blocked bool `json:"blocked"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (l *PeerLogEntry) UnmarshalJSON(data []byte) error {
|
||||
var raw rawPeerLogEntry
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
t := time.Unix(0, int64(raw.Timestamp)*int64(time.Millisecond))
|
||||
*l = PeerLogEntry{
|
||||
ID: raw.ID,
|
||||
IP: raw.IP,
|
||||
Timestamp: t,
|
||||
Blocked: raw.Blocked,
|
||||
Reason: raw.Reason,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rawPeerLogEntry struct {
|
||||
ID int `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Blocked bool `json:"blocked"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type Preferences struct {
|
||||
// Currently selected language (e.g. en_GB for English)
|
||||
Locale string `json:"locale"`
|
||||
// True if a subfolder should be created when adding a torrent
|
||||
CreateSubfolderEnabled bool `json:"create_subfolder_enabled"`
|
||||
// True if torrents should be added in a Paused state
|
||||
StartPausedEnabled bool `json:"start_paused_enabled"`
|
||||
// No documentation provided
|
||||
AutoDeleteMode int `json:"auto_delete_mode"`
|
||||
// True if disk space should be pre-allocated for all files
|
||||
PreallocateAll bool `json:"preallocate_all"`
|
||||
// True if ".!qB" should be appended to incomplete files
|
||||
IncompleteFilesExt bool `json:"incomplete_files_ext"`
|
||||
// True if Automatic Torrent Management is enabled by default
|
||||
AutoTmmEnabled bool `json:"auto_tmm_enabled"`
|
||||
// True if torrent should be relocated when its Category changes
|
||||
TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled"`
|
||||
// True if torrent should be relocated when the default save path changes
|
||||
SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled"`
|
||||
// True if torrent should be relocated when its Category's save path changes
|
||||
CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled"`
|
||||
// Default save path for torrents, separated by slashes
|
||||
SavePath string `json:"save_path"`
|
||||
// True if folder for incomplete torrents is enabled
|
||||
TempPathEnabled bool `json:"temp_path_enabled"`
|
||||
// Path for incomplete torrents, separated by slashes
|
||||
TempPath string `json:"temp_path"`
|
||||
// Property: directory to watch for torrent files, value: where torrents loaded from this directory should be downloaded to (see list of possible values below). Slashes are used as path separators; multiple key/value pairs can be specified
|
||||
ScanDirs map[string]interface{} `json:"scan_dirs"`
|
||||
// Path to directory to copy .torrent files to. Slashes are used as path separators
|
||||
ExportDir string `json:"export_dir"`
|
||||
// Path to directory to copy .torrent files of completed downloads to. Slashes are used as path separators
|
||||
ExportDirFin string `json:"export_dir_fin"`
|
||||
// True if e-mail notification should be enabled
|
||||
MailNotificationEnabled bool `json:"mail_notification_enabled"`
|
||||
// e-mail where notifications should originate from
|
||||
MailNotificationSender string `json:"mail_notification_sender"`
|
||||
// e-mail to send notifications to
|
||||
MailNotificationEmail string `json:"mail_notification_email"`
|
||||
// smtp server for e-mail notifications
|
||||
MailNotificationSmtp string `json:"mail_notification_smtp"`
|
||||
// True if smtp server requires SSL connection
|
||||
MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled"`
|
||||
// True if smtp server requires authentication
|
||||
MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled"`
|
||||
// Username for smtp authentication
|
||||
MailNotificationUsername string `json:"mail_notification_username"`
|
||||
// Password for smtp authentication
|
||||
MailNotificationPassword string `json:"mail_notification_password"`
|
||||
// True if external program should be run after torrent has finished downloading
|
||||
AutorunEnabled bool `json:"autorun_enabled"`
|
||||
// Program path/name/arguments to run if autorun_enabled is enabled; path is separated by slashes; you can use %f and %n arguments, which will be expanded by qBittorent as path_to_torrent_file and torrent_name (from the GUI; not the .torrent file name) respectively
|
||||
AutorunProgram string `json:"autorun_program"`
|
||||
// True if torrent queuing is enabled
|
||||
QueueingEnabled bool `json:"queueing_enabled"`
|
||||
// Maximum number of active simultaneous downloads
|
||||
MaxActiveDownloads int `json:"max_active_downloads"`
|
||||
// Maximum number of active simultaneous downloads and uploads
|
||||
MaxActiveTorrents int `json:"max_active_torrents"`
|
||||
// Maximum number of active simultaneous uploads
|
||||
MaxActiveUploads int `json:"max_active_uploads"`
|
||||
// If true torrents w/o any activity (stalled ones) will not be counted towards max_active_* limits; see dont_count_slow_torrents for more information
|
||||
DontCountSlowTorrents bool `json:"dont_count_slow_torrents"`
|
||||
// Download rate in KiB/s for a torrent to be considered "slow"
|
||||
SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold"`
|
||||
// Upload rate in KiB/s for a torrent to be considered "slow"
|
||||
SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold"`
|
||||
// Seconds a torrent should be inactive before considered "slow"
|
||||
SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer"`
|
||||
// True if share ratio limit is enabled
|
||||
MaxRatioEnabled bool `json:"max_ratio_enabled"`
|
||||
// Get the global share ratio limit
|
||||
MaxRatio float64 `json:"max_ratio"`
|
||||
// Action performed when a torrent reaches the maximum share ratio. See list of possible values here below.
|
||||
MaxRatioAct MaxRatioAction `json:"max_ratio_act"`
|
||||
// Port for incoming connections
|
||||
ListenPort int `json:"listen_port"`
|
||||
// True if UPnP/NAT-PMP is enabled
|
||||
Upnp bool `json:"upnp"`
|
||||
// True if the port is randomly selected
|
||||
RandomPort bool `json:"random_port"`
|
||||
// Global download speed limit in KiB/s; -1 means no limit is applied
|
||||
DlLimit int `json:"dl_limit"`
|
||||
// Global upload speed limit in KiB/s; -1 means no limit is applied
|
||||
UpLimit int `json:"up_limit"`
|
||||
// Maximum global number of simultaneous connections
|
||||
MaxConnec int `json:"max_connec"`
|
||||
// Maximum number of simultaneous connections per torrent
|
||||
MaxConnecPerTorrent int `json:"max_connec_per_torrent"`
|
||||
// Maximum number of upload slots
|
||||
MaxUploads int `json:"max_uploads"`
|
||||
// Maximum number of upload slots per torrent
|
||||
MaxUploadsPerTorrent int `json:"max_uploads_per_torrent"`
|
||||
// True if uTP protocol should be enabled; this option is only available in qBittorent built against libtorrent version 0.16.X and higher
|
||||
EnableUtp bool `json:"enable_utp"`
|
||||
// True if [du]l_limit should be applied to uTP connections; this option is only available in qBittorent built against libtorrent version 0.16.X and higher
|
||||
LimitUtpRate bool `json:"limit_utp_rate"`
|
||||
// True if [du]l_limit should be applied to estimated TCP overhead (service data: e.g. packet headers)
|
||||
LimitTcpOverhead bool `json:"limit_tcp_overhead"`
|
||||
// True if [du]l_limit should be applied to peers on the LAN
|
||||
LimitLanPeers bool `json:"limit_lan_peers"`
|
||||
// Alternative global download speed limit in KiB/s
|
||||
AltDlLimit int `json:"alt_dl_limit"`
|
||||
// Alternative global upload speed limit in KiB/s
|
||||
AltUpLimit int `json:"alt_up_limit"`
|
||||
// True if alternative limits should be applied according to schedule
|
||||
SchedulerEnabled bool `json:"scheduler_enabled"`
|
||||
// Scheduler starting hour
|
||||
ScheduleFromHour int `json:"schedule_from_hour"`
|
||||
// Scheduler starting minute
|
||||
ScheduleFromMin int `json:"schedule_from_min"`
|
||||
// Scheduler ending hour
|
||||
ScheduleToHour int `json:"schedule_to_hour"`
|
||||
// Scheduler ending minute
|
||||
ScheduleToMin int `json:"schedule_to_min"`
|
||||
// Scheduler days. See possible values here below
|
||||
SchedulerDays int `json:"scheduler_days"`
|
||||
// True if DHT is enabled
|
||||
Dht bool `json:"dht"`
|
||||
// True if DHT port should match TCP port
|
||||
DhtSameAsBT bool `json:"dhtSameAsBT"`
|
||||
// DHT port if dhtSameAsBT is false
|
||||
DhtPort int `json:"dht_port"`
|
||||
// True if PeX is enabled
|
||||
Pex bool `json:"pex"`
|
||||
// True if LSD is enabled
|
||||
Lsd bool `json:"lsd"`
|
||||
// See list of possible values here below
|
||||
Encryption int `json:"encryption"`
|
||||
// If true anonymous mode will be enabled; read more here; this option is only available in qBittorent built against libtorrent version 0.16.X and higher
|
||||
AnonymousMode bool `json:"anonymous_mode"`
|
||||
// See list of possible values here below
|
||||
ProxyType int `json:"proxy_type"`
|
||||
// Proxy IP address or domain name
|
||||
ProxyIp string `json:"proxy_ip"`
|
||||
// Proxy port
|
||||
ProxyPort int `json:"proxy_port"`
|
||||
// True if peer and web seed connections should be proxified; this option will have any effect only in qBittorent built against libtorrent version 0.16.X and higher
|
||||
ProxyPeerConnections bool `json:"proxy_peer_connections"`
|
||||
// True if the connections not supported by the proxy are disabled
|
||||
ForceProxy bool `json:"force_proxy"`
|
||||
// True proxy requires authentication; doesn't apply to SOCKS4 proxies
|
||||
ProxyAuthEnabled bool `json:"proxy_auth_enabled"`
|
||||
// Username for proxy authentication
|
||||
ProxyUsername string `json:"proxy_username"`
|
||||
// Password for proxy authentication
|
||||
ProxyPassword string `json:"proxy_password"`
|
||||
// True if external IP filter should be enabled
|
||||
IpFilterEnabled bool `json:"ip_filter_enabled"`
|
||||
// Path to IP filter file (.dat, .p2p, .p2b files are supported); path is separated by slashes
|
||||
IpFilterPath string `json:"ip_filter_path"`
|
||||
// True if IP filters are applied to trackers
|
||||
IpFilterTrackers bool `json:"ip_filter_trackers"`
|
||||
// Comma-separated list of domains to accept when performing Host header validation
|
||||
WebUiDomainList string `json:"web_ui_domain_list"`
|
||||
// IP address to use for the WebUI
|
||||
WebUiAddress string `json:"web_ui_address"`
|
||||
// WebUI port
|
||||
WebUiPort int `json:"web_ui_port"`
|
||||
// True if UPnP is used for the WebUI port
|
||||
WebUiUpnp bool `json:"web_ui_upnp"`
|
||||
// WebUI username
|
||||
WebUiUsername string `json:"web_ui_username"`
|
||||
// For API ≥ v2.3.0: Plaintext WebUI password, not readable, write-only. For API < v2.3.0: MD5 hash of WebUI password, hash is generated from the following string: username:Web UI Access:plain_text_web_ui_password
|
||||
WebUiPassword string `json:"web_ui_password"`
|
||||
// True if WebUI CSRF protection is enabled
|
||||
WebUiCsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled"`
|
||||
// True if WebUI clickjacking protection is enabled
|
||||
WebUiClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled"`
|
||||
// True if authentication challenge for loopback address (127.0.0.1) should be disabled
|
||||
BypassLocalAuth bool `json:"bypass_local_auth"`
|
||||
// True if webui authentication should be bypassed for clients whose ip resides within (at least) one of the subnets on the whitelist
|
||||
BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled"`
|
||||
// (White)list of ipv4/ipv6 subnets for which webui authentication should be bypassed; list entries are separated by commas
|
||||
BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist"`
|
||||
// True if an alternative WebUI should be used
|
||||
AlternativeWebuiEnabled bool `json:"alternative_webui_enabled"`
|
||||
// File path to the alternative WebUI
|
||||
AlternativeWebuiPath string `json:"alternative_webui_path"`
|
||||
// True if WebUI HTTPS access is enabled
|
||||
UseHttps bool `json:"use_https"`
|
||||
// SSL keyfile contents (this is a not a path)
|
||||
SslKey string `json:"ssl_key"`
|
||||
// SSL certificate contents (this is a not a path)
|
||||
SslCert string `json:"ssl_cert"`
|
||||
// True if server DNS should be updated dynamically
|
||||
DyndnsEnabled bool `json:"dyndns_enabled"`
|
||||
// See list of possible values here below
|
||||
DyndnsService int `json:"dyndns_service"`
|
||||
// Username for DDNS service
|
||||
DyndnsUsername string `json:"dyndns_username"`
|
||||
// Password for DDNS service
|
||||
DyndnsPassword string `json:"dyndns_password"`
|
||||
// Your DDNS domain name
|
||||
DyndnsDomain string `json:"dyndns_domain"`
|
||||
// RSS refresh interval
|
||||
RssRefreshInterval int `json:"rss_refresh_interval"`
|
||||
// Max stored articles per RSS feed
|
||||
RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed"`
|
||||
// Enable processing of RSS feeds
|
||||
RssProcessingEnabled bool `json:"rss_processing_enabled"`
|
||||
// Enable auto-downloading of torrents from the RSS feeds
|
||||
RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled"`
|
||||
}
|
||||
|
||||
type MaxRatioAction int
|
||||
|
||||
const (
|
||||
ActionPause MaxRatioAction = 0
|
||||
ActionRemove = 1
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type RuleDefinition struct {
|
||||
// Whether the rule is enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
// The substring that the torrent name must contain
|
||||
MustContain string `json:"mustContain"`
|
||||
// The substring that the torrent name must not contain
|
||||
MustNotContain string `json:"mustNotContain"`
|
||||
// Enable regex mode in "mustContain" and "mustNotContain"
|
||||
UseRegex bool `json:"useRegex"`
|
||||
// Episode filter definition
|
||||
EpisodeFilter string `json:"episodeFilter"`
|
||||
// Enable smart episode filter
|
||||
SmartFilter bool `json:"smartFilter"`
|
||||
// The list of episode IDs already matched by smart filter
|
||||
PreviouslyMatchedEpisodes []string `json:"previouslyMatchedEpisodes"`
|
||||
// The feed URLs the rule applied to
|
||||
AffectedFeeds []string `json:"affectedFeeds"`
|
||||
// Ignore sunsequent rule matches
|
||||
IgnoreDays int `json:"ignoreDays"`
|
||||
// The rule last match time
|
||||
LastMatch string `json:"lastMatch"`
|
||||
// Add matched torrent in paused mode
|
||||
AddPaused bool `json:"addPaused"`
|
||||
// Assign category to the torrent
|
||||
AssignedCategory string `json:"assignedCategory"`
|
||||
// Save torrent to the given directory
|
||||
SavePath string `json:"savePath"`
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SearchPlugin struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
FullName string `json:"fullName"`
|
||||
Name string `json:"name"`
|
||||
SupportedCategories []string `json:"supportedCategories"`
|
||||
URL string `json:"url"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SearchResult struct {
|
||||
// URL of the torrent's description page
|
||||
DescriptionLink string `json:"descrLink"`
|
||||
// Name of the file
|
||||
FileName string `json:"fileName"`
|
||||
// Size of the file in Bytes
|
||||
FileSize int `json:"fileSize"`
|
||||
// Torrent download link (usually either .torrent file or magnet link)
|
||||
FileUrl string `json:"fileUrl"`
|
||||
// Number of leechers
|
||||
NumLeechers int `json:"nbLeechers"`
|
||||
// int of seeders
|
||||
NumSeeders int `json:"nbSeeders"`
|
||||
// URL of the torrent site
|
||||
SiteUrl string `json:"siteUrl"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SearchResultsPaging struct {
|
||||
Results []SearchResult `json:"results"`
|
||||
Status string `json:"status"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SearchStatus struct {
|
||||
ID int `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package qbittorrent_model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ServerState struct {
|
||||
TransferInfo
|
||||
AlltimeDl int `json:"alltime_dl"`
|
||||
AlltimeUl int `json:"alltime_ul"`
|
||||
AverageTimeQueue int `json:"average_time_queue"`
|
||||
FreeSpaceOnDisk int `json:"free_space_on_disk"`
|
||||
GlobalRatio float64 `json:"global_ratio"`
|
||||
QueuedIoJobs int `json:"queued_io_jobs"`
|
||||
ReadCacheHits float64 `json:"read_cache_hits"`
|
||||
ReadCacheOverload float64 `json:"read_cache_overload"`
|
||||
TotalBuffersSize int `json:"total_buffers_size"`
|
||||
TotalPeerConnections int `json:"total_peer_connections"`
|
||||
TotalQueuedSize int `json:"total_queued_size"`
|
||||
TotalWastedSession int `json:"total_wasted_session"`
|
||||
WriteCacheOverload float64 `json:"write_cache_overload"`
|
||||
}
|
||||
|
||||
func (s *ServerState) UnmarshalJSON(data []byte) error {
|
||||
var raw rawServerState
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
globalRatio, err := strconv.ParseFloat(raw.GlobalRatio, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readCacheHits, err := strconv.ParseFloat(raw.ReadCacheHits, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readCacheOverload, err := strconv.ParseFloat(raw.ReadCacheOverload, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeCacheOverload, err := strconv.ParseFloat(raw.WriteCacheOverload, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = ServerState{
|
||||
TransferInfo: raw.TransferInfo,
|
||||
AlltimeDl: raw.AlltimeDl,
|
||||
AlltimeUl: raw.AlltimeUl,
|
||||
AverageTimeQueue: raw.AverageTimeQueue,
|
||||
FreeSpaceOnDisk: raw.FreeSpaceOnDisk,
|
||||
GlobalRatio: globalRatio,
|
||||
QueuedIoJobs: raw.QueuedIoJobs,
|
||||
ReadCacheHits: readCacheHits,
|
||||
ReadCacheOverload: readCacheOverload,
|
||||
TotalBuffersSize: raw.TotalBuffersSize,
|
||||
TotalPeerConnections: raw.TotalPeerConnections,
|
||||
TotalQueuedSize: raw.TotalQueuedSize,
|
||||
TotalWastedSession: raw.TotalWastedSession,
|
||||
WriteCacheOverload: writeCacheOverload,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rawServerState struct {
|
||||
TransferInfo
|
||||
AlltimeDl int `json:"alltime_dl"`
|
||||
AlltimeUl int `json:"alltime_ul"`
|
||||
AverageTimeQueue int `json:"average_time_queue"`
|
||||
FreeSpaceOnDisk int `json:"free_space_on_disk"`
|
||||
GlobalRatio string `json:"global_ratio"`
|
||||
QueuedIoJobs int `json:"queued_io_jobs"`
|
||||
ReadCacheHits string `json:"read_cache_hits"`
|
||||
ReadCacheOverload string `json:"read_cache_overload"`
|
||||
TotalBuffersSize int `json:"total_buffers_size"`
|
||||
TotalPeerConnections int `json:"total_peer_connections"`
|
||||
TotalQueuedSize int `json:"total_queued_size"`
|
||||
TotalWastedSession int `json:"total_wasted_session"`
|
||||
WriteCacheOverload string `json:"write_cache_overload"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SyncMainData struct {
|
||||
RID int `json:"rid"`
|
||||
FullUpdate bool `json:"full_update"`
|
||||
Torrents map[string]*Torrent `json:"torrents"`
|
||||
TorrentsRemoved []string `json:"torrents_removed"`
|
||||
Categories map[string]Category `json:"categories"`
|
||||
CategoriesRemoved map[string]Category `json:"categories_removed"`
|
||||
Queueing bool `json:"queueing"`
|
||||
ServerState ServerState `json:"server_state"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type SyncPeersData struct {
|
||||
FullUpdate bool `json:"full_update"`
|
||||
Peers map[string]Peer `json:"peers"`
|
||||
RID int `json:"rid"`
|
||||
ShowFlags bool `json:"show_flags"`
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type Torrent struct {
|
||||
// Torrent hash
|
||||
Hash string `json:"hash"`
|
||||
// Torrent name
|
||||
Name string `json:"name"`
|
||||
// Total size (bytes) of files selected for download
|
||||
Size int `json:"size"`
|
||||
// Torrent progress (percentage/100)
|
||||
Progress float64 `json:"progress"`
|
||||
// Torrent download speed (bytes/s)
|
||||
Dlspeed int `json:"dlspeed"`
|
||||
// Torrent upload speed (bytes/s)
|
||||
Upspeed int `json:"upspeed"`
|
||||
// Torrent priority. Returns -1 if queuing is disabled or torrent is in seed mode
|
||||
Priority int `json:"priority"`
|
||||
// Number of seeds connected to
|
||||
NumSeeds int `json:"num_seeds"`
|
||||
// Number of seeds in the swarm
|
||||
NumComplete int `json:"num_complete"`
|
||||
// Number of leechers connected to
|
||||
NumLeechs int `json:"num_leechs"`
|
||||
// Number of leechers in the swarm
|
||||
NumIncomplete int `json:"num_incomplete"`
|
||||
// Torrent share ratio. Max ratio value: 9999.
|
||||
Ratio float64 `json:"ratio"`
|
||||
// Torrent ETA (seconds)
|
||||
Eta int `json:"eta"`
|
||||
// Torrent state. See table here below for the possible values
|
||||
State TorrentState `json:"state"`
|
||||
// True if sequential download is enabled
|
||||
SeqDl bool `json:"seq_dl"`
|
||||
// True if first last piece are prioritized
|
||||
FLPiecePrio bool `json:"f_l_piece_prio"`
|
||||
// Category of the torrent
|
||||
Category string `json:"category"`
|
||||
// True if super seeding is enabled
|
||||
SuperSeeding bool `json:"super_seeding"`
|
||||
// True if force start is enabled for this torrent
|
||||
ForceStart bool `json:"force_start"`
|
||||
|
||||
// New added fields
|
||||
AddedOn int `json:"added_on"`
|
||||
AmountLeft int `json:"amount_left"`
|
||||
AutoTmm bool `json:"auto_tmm"`
|
||||
Availability float64 `json:"availability"`
|
||||
Completed int64 `json:"completed"`
|
||||
CompletionOn int `json:"completion_on"`
|
||||
ContentPath string `json:"content_path"`
|
||||
DlLimit int `json:"dl_limit"`
|
||||
DownloadPath string `json:"download_path"`
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
DownloadedSession int `json:"downloaded_session"`
|
||||
InfohashV1 string `json:"infohash_v1"`
|
||||
InfohashV2 string `json:"infohash_v2"`
|
||||
LastActivity int `json:"last_activity"`
|
||||
MagnetUri string `json:"magnet_uri"`
|
||||
MaxRatio int `json:"max_ratio"`
|
||||
MaxSeedingTime int `json:"max_seeding_time"`
|
||||
RatioLimit int `json:"ratio_limit"`
|
||||
SavePath string `json:"save_path"`
|
||||
SeedingTime int `json:"seeding_time"`
|
||||
SeedingTimeLimit int `json:"seeding_time_limit"`
|
||||
SeenComplete int `json:"seen_complete"`
|
||||
Tags string `json:"tags"`
|
||||
TimeActive int `json:"time_active"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
Tracker string `json:"tracker"`
|
||||
TrackersCount int `json:"trackers_count"`
|
||||
UpLimit int `json:"up_limit"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
UploadedSession int64 `json:"uploaded_session"`
|
||||
}
|
||||
|
||||
type TorrentState string
|
||||
|
||||
const (
|
||||
// Some error occurred, applies to paused torrents
|
||||
StateError TorrentState = "error"
|
||||
// Torrent data files is missing
|
||||
StateMissingFiles TorrentState = "missingFiles"
|
||||
// Torrent is being seeded and data is being transferred
|
||||
StateUploading TorrentState = "uploading"
|
||||
// Torrent is paused and has finished downloading
|
||||
StatePausedUP TorrentState = "pausedUP"
|
||||
StateStoppedUP TorrentState = "stoppedUP"
|
||||
// Queuing is enabled and torrent is queued for upload
|
||||
StateQueuedUP TorrentState = "queuedUP"
|
||||
// Torrent is being seeded, but no connection were made
|
||||
StateStalledUP TorrentState = "stalledUP"
|
||||
// Torrent has finished downloading and is being checked
|
||||
StateCheckingUP TorrentState = "checkingUP"
|
||||
// Torrent is forced to uploading and ignore queue limit
|
||||
StateForcedUP TorrentState = "forcedUP"
|
||||
// Torrent is allocating disk space for download
|
||||
StateAllocating TorrentState = "allocating"
|
||||
// Torrent is being downloaded and data is being transferred
|
||||
StateDownloading TorrentState = "downloading"
|
||||
// Torrent has just started downloading and is fetching metadata
|
||||
StateMetaDL TorrentState = "metaDL"
|
||||
// Torrent is paused and has NOT finished downloading
|
||||
StatePausedDL TorrentState = "pausedDL"
|
||||
StateStoppedDL TorrentState = "stoppedDL"
|
||||
// Queuing is enabled and torrent is queued for download
|
||||
StateQueuedDL TorrentState = "queuedDL"
|
||||
// Torrent is being downloaded, but no connection were made
|
||||
StateStalledDL TorrentState = "stalledDL"
|
||||
// Same as checkingUP, but torrent has NOT finished downloading
|
||||
StateCheckingDL TorrentState = "checkingDL"
|
||||
// Torrent is forced to downloading to ignore queue limit
|
||||
StateForceDL TorrentState = "forceDL"
|
||||
// Checking resume data on qBt startup
|
||||
StateCheckingResumeData TorrentState = "checkingResumeData"
|
||||
// Torrent is moving to another location
|
||||
StateMoving TorrentState = "moving"
|
||||
// Unknown status
|
||||
StateUnknown TorrentState = "unknown"
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type TorrentContent struct {
|
||||
// File name (including relative path)
|
||||
Name string `json:" name"`
|
||||
// File size (bytes)
|
||||
Size int `json:" size"`
|
||||
// File progress (percentage/100)
|
||||
Progress float64 `json:" progress"`
|
||||
// File priority. See possible values here below
|
||||
Priority TorrentPriority `json:" priority"`
|
||||
// True if file is seeding/complete
|
||||
IsSeed bool `json:" is_seed"`
|
||||
// The first number is the starting piece index and the second number is the ending piece index (inclusive)
|
||||
PieceRange []int `json:" piece_range"`
|
||||
// Percentage of file pieces currently available
|
||||
Availability float64 `json:" availability"`
|
||||
}
|
||||
|
||||
type TorrentPriority int
|
||||
|
||||
const (
|
||||
PriorityDoNotDownload TorrentPriority = 0
|
||||
PriorityNormal = 1
|
||||
PriorityHigh = 6
|
||||
PriorityMaximum = 7
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type TorrentPieceState int
|
||||
|
||||
const (
|
||||
PieceStateNotDownloaded TorrentPieceState = iota
|
||||
PieceStateDownloading
|
||||
PieceStateDownloaded
|
||||
)
|
||||
@@ -0,0 +1,194 @@
|
||||
package qbittorrent_model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TorrentProperties struct {
|
||||
// Torrent save path
|
||||
SavePath string `json:"save_path"`
|
||||
// Torrent creation date (Unix timestamp)
|
||||
CreationDate time.Time `json:"creation_date"`
|
||||
// Torrent piece size (bytes)
|
||||
PieceSize int `json:"piece_size"`
|
||||
// Torrent comment
|
||||
Comment string `json:"comment"`
|
||||
// Total data wasted for torrent (bytes)
|
||||
TotalWasted int `json:"total_wasted"`
|
||||
// Total data uploaded for torrent (bytes)
|
||||
TotalUploaded int `json:"total_uploaded"`
|
||||
// Total data uploaded this session (bytes)
|
||||
TotalUploadedSession int `json:"total_uploaded_session"`
|
||||
// Total data downloaded for torrent (bytes)
|
||||
TotalDownloaded int `json:"total_downloaded"`
|
||||
// Total data downloaded this session (bytes)
|
||||
TotalDownloadedSession int `json:"total_downloaded_session"`
|
||||
// Torrent upload limit (bytes/s)
|
||||
UpLimit int `json:"up_limit"`
|
||||
// Torrent download limit (bytes/s)
|
||||
DlLimit int `json:"dl_limit"`
|
||||
// Torrent elapsed time (seconds)
|
||||
TimeElapsed int `json:"time_elapsed"`
|
||||
// Torrent elapsed time while complete (seconds)
|
||||
SeedingTime time.Duration `json:"seeding_time"`
|
||||
// Torrent connection count
|
||||
NbConnections int `json:"nb_connections"`
|
||||
// Torrent connection count limit
|
||||
NbConnectionsLimit int `json:"nb_connections_limit"`
|
||||
// Torrent share ratio
|
||||
ShareRatio float64 `json:"share_ratio"`
|
||||
// When this torrent was added (unix timestamp)
|
||||
AdditionDate time.Time `json:"addition_date"`
|
||||
// Torrent completion date (unix timestamp)
|
||||
CompletionDate time.Time `json:"completion_date"`
|
||||
// Torrent creator
|
||||
CreatedBy string `json:"created_by"`
|
||||
// Torrent average download speed (bytes/second)
|
||||
DlSpeedAvg int `json:"dl_speed_avg"`
|
||||
// Torrent download speed (bytes/second)
|
||||
DlSpeed int `json:"dl_speed"`
|
||||
// Torrent ETA (seconds)
|
||||
Eta time.Duration `json:"eta"`
|
||||
// Last seen complete date (unix timestamp)
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
// Number of peers connected to
|
||||
Peers int `json:"peers"`
|
||||
// Number of peers in the swarm
|
||||
PeersTotal int `json:"peers_total"`
|
||||
// Number of pieces owned
|
||||
PiecesHave int `json:"pieces_have"`
|
||||
// Number of pieces of the torrent
|
||||
PiecesNum int `json:"pieces_num"`
|
||||
// Number of seconds until the next announce
|
||||
Reannounce time.Duration `json:"reannounce"`
|
||||
// Number of seeds connected to
|
||||
Seeds int `json:"seeds"`
|
||||
// Number of seeds in the swarm
|
||||
SeedsTotal int `json:"seeds_total"`
|
||||
// Torrent total size (bytes)
|
||||
TotalSize int `json:"total_size"`
|
||||
// Torrent average upload speed (bytes/second)
|
||||
UpSpeedAvg int `json:"up_speed_avg"`
|
||||
// Torrent upload speed (bytes/second)
|
||||
UpSpeed int `json:"up_speed"`
|
||||
}
|
||||
|
||||
func (p *TorrentProperties) UnmarshalJSON(data []byte) error {
|
||||
var raw rawTorrentProperties
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
creationDate := time.Unix(int64(raw.CreationDate), 0)
|
||||
seedingTime := time.Duration(raw.SeedingTime) * time.Second
|
||||
additionDate := time.Unix(int64(raw.AdditionDate), 0)
|
||||
completionDate := time.Unix(int64(raw.CompletionDate), 0)
|
||||
eta := time.Duration(raw.Eta) * time.Second
|
||||
lastSeen := time.Unix(int64(raw.LastSeen), 0)
|
||||
reannounce := time.Duration(raw.Reannounce) * time.Second
|
||||
*p = TorrentProperties{
|
||||
SavePath: raw.SavePath,
|
||||
CreationDate: creationDate,
|
||||
PieceSize: raw.PieceSize,
|
||||
Comment: raw.Comment,
|
||||
TotalWasted: raw.TotalWasted,
|
||||
TotalUploaded: raw.TotalUploaded,
|
||||
TotalUploadedSession: raw.TotalUploadedSession,
|
||||
TotalDownloaded: raw.TotalDownloaded,
|
||||
TotalDownloadedSession: raw.TotalDownloadedSession,
|
||||
UpLimit: raw.UpLimit,
|
||||
DlLimit: raw.DlLimit,
|
||||
TimeElapsed: raw.TimeElapsed,
|
||||
SeedingTime: seedingTime,
|
||||
NbConnections: raw.NbConnections,
|
||||
NbConnectionsLimit: raw.NbConnectionsLimit,
|
||||
ShareRatio: raw.ShareRatio,
|
||||
AdditionDate: additionDate,
|
||||
CompletionDate: completionDate,
|
||||
CreatedBy: raw.CreatedBy,
|
||||
DlSpeedAvg: raw.DlSpeedAvg,
|
||||
DlSpeed: raw.DlSpeed,
|
||||
Eta: eta,
|
||||
LastSeen: lastSeen,
|
||||
Peers: raw.Peers,
|
||||
PeersTotal: raw.PeersTotal,
|
||||
PiecesHave: raw.PiecesHave,
|
||||
PiecesNum: raw.PiecesNum,
|
||||
Reannounce: reannounce,
|
||||
Seeds: raw.Seeds,
|
||||
SeedsTotal: raw.SeedsTotal,
|
||||
TotalSize: raw.TotalSize,
|
||||
UpSpeedAvg: raw.UpSpeedAvg,
|
||||
UpSpeed: raw.UpSpeed,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rawTorrentProperties struct {
|
||||
// Torrent save path
|
||||
SavePath string `json:"save_path"`
|
||||
// Torrent creation date (Unix timestamp)
|
||||
CreationDate int `json:"creation_date"`
|
||||
// Torrent piece size (bytes)
|
||||
PieceSize int `json:"piece_size"`
|
||||
// Torrent comment
|
||||
Comment string `json:"comment"`
|
||||
// Total data wasted for torrent (bytes)
|
||||
TotalWasted int `json:"total_wasted"`
|
||||
// Total data uploaded for torrent (bytes)
|
||||
TotalUploaded int `json:"total_uploaded"`
|
||||
// Total data uploaded this session (bytes)
|
||||
TotalUploadedSession int `json:"total_uploaded_session"`
|
||||
// Total data downloaded for torrent (bytes)
|
||||
TotalDownloaded int `json:"total_downloaded"`
|
||||
// Total data downloaded this session (bytes)
|
||||
TotalDownloadedSession int `json:"total_downloaded_session"`
|
||||
// Torrent upload limit (bytes/s)
|
||||
UpLimit int `json:"up_limit"`
|
||||
// Torrent download limit (bytes/s)
|
||||
DlLimit int `json:"dl_limit"`
|
||||
// Torrent elapsed time (seconds)
|
||||
TimeElapsed int `json:"time_elapsed"`
|
||||
// Torrent elapsed time while complete (seconds)
|
||||
SeedingTime int `json:"seeding_time"`
|
||||
// Torrent connection count
|
||||
NbConnections int `json:"nb_connections"`
|
||||
// Torrent connection count limit
|
||||
NbConnectionsLimit int `json:"nb_connections_limit"`
|
||||
// Torrent share ratio
|
||||
ShareRatio float64 `json:"share_ratio"`
|
||||
// When this torrent was added (unix timestamp)
|
||||
AdditionDate int `json:"addition_date"`
|
||||
// Torrent completion date (unix timestamp)
|
||||
CompletionDate int `json:"completion_date"`
|
||||
// Torrent creator
|
||||
CreatedBy string `json:"created_by"`
|
||||
// Torrent average download speed (bytes/second)
|
||||
DlSpeedAvg int `json:"dl_speed_avg"`
|
||||
// Torrent download speed (bytes/second)
|
||||
DlSpeed int `json:"dl_speed"`
|
||||
// Torrent ETA (seconds)
|
||||
Eta int `json:"eta"`
|
||||
// Last seen complete date (unix timestamp)
|
||||
LastSeen int `json:"last_seen"`
|
||||
// Number of peers connected to
|
||||
Peers int `json:"peers"`
|
||||
// Number of peers in the swarm
|
||||
PeersTotal int `json:"peers_total"`
|
||||
// Number of pieces owned
|
||||
PiecesHave int `json:"pieces_have"`
|
||||
// Number of pieces of the torrent
|
||||
PiecesNum int `json:"pieces_num"`
|
||||
// Number of seconds until the next announce
|
||||
Reannounce int `json:"reannounce"`
|
||||
// Number of seeds connected to
|
||||
Seeds int `json:"seeds"`
|
||||
// Number of seeds in the swarm
|
||||
SeedsTotal int `json:"seeds_total"`
|
||||
// Torrent total size (bytes)
|
||||
TotalSize int `json:"total_size"`
|
||||
// Torrent average upload speed (bytes/second)
|
||||
UpSpeedAvg int `json:"up_speed_avg"`
|
||||
// Torrent upload speed (bytes/second)
|
||||
UpSpeed int `json:"up_speed"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type TorrentTracker struct {
|
||||
URL string `json:"url"`
|
||||
Status TrackerStatus `json:"status"`
|
||||
Tier int `json:"tier"`
|
||||
NumPeers int `json:"num_peers"`
|
||||
NumSeeds int `json:"num_seeds"`
|
||||
NumLeeches int `json:"num_leeches"`
|
||||
NumDownloaded int `json:"num_downloaded"`
|
||||
Message string `json:"msg"`
|
||||
}
|
||||
|
||||
type TrackerStatus int
|
||||
|
||||
const (
|
||||
TrackerStatusDisabled TrackerStatus = iota
|
||||
TrackerStatusNotContacted
|
||||
TrackerStatusWorking
|
||||
TrackerStatusUpdating
|
||||
TrackerStatusNotWorking
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package qbittorrent_model
|
||||
|
||||
type TransferInfo struct {
|
||||
ConnectionStatus ConnectionStatus `json:"connection_status"`
|
||||
DhtNodes int `json:"dht_nodes"`
|
||||
DlInfoData int `json:"dl_info_data"`
|
||||
DlInfoSpeed int `json:"dl_info_speed"`
|
||||
DlRateLimit int `json:"dl_rate_limit"`
|
||||
UpInfoData int `json:"up_info_data"`
|
||||
UpInfoSpeed int `json:"up_info_speed"`
|
||||
UpRateLimit int `json:"up_rate_limit"`
|
||||
UseAltSpeedLimits bool `json:"use_alt_speed_limits"`
|
||||
Queueing bool `json:"queueing"`
|
||||
RefreshInterval int `json:"refresh_interval"`
|
||||
}
|
||||
|
||||
type ConnectionStatus string
|
||||
|
||||
const (
|
||||
StatusConnected ConnectionStatus = "connected"
|
||||
StatusFirewalled = "firewalled"
|
||||
StatusDisconnected = "disconnected"
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
package qbittorrent_rss
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
qbittorrent_model "seanime/internal/torrent_clients/qbittorrent/model"
|
||||
qbittorrent_util "seanime/internal/torrent_clients/qbittorrent/util"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) AddFolder(folder string) error {
|
||||
params := url.Values{}
|
||||
params.Add("path", folder)
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/addFolder?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) AddFeed(link string, folder string) error {
|
||||
params := url.Values{}
|
||||
params.Add("path", folder)
|
||||
if folder != "" {
|
||||
params.Add("path", folder)
|
||||
}
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/addFeed?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RemoveItem(folder string) error {
|
||||
params := url.Values{}
|
||||
params.Add("path", folder)
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/removeItem?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) MoveItem(currentFolder, destinationFolder string) error {
|
||||
params := url.Values{}
|
||||
params.Add("itemPath", currentFolder)
|
||||
params.Add("destPath", destinationFolder)
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/moveItem?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) AddRule(name string, def qbittorrent_model.RuleDefinition) error {
|
||||
params := url.Values{}
|
||||
b, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params.Add("ruleName", name)
|
||||
params.Add("ruleDef", string(b))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/setRule?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RenameRule(old, new string) error {
|
||||
params := url.Values{}
|
||||
params.Add("ruleName", old)
|
||||
params.Add("newRuleName", new)
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/renameRule?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RemoveRule(name string) error {
|
||||
params := url.Values{}
|
||||
params.Add("ruleName", name)
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/removeRule?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetRules() (map[string]qbittorrent_model.RuleDefinition, error) {
|
||||
var res map[string]qbittorrent_model.RuleDefinition
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/rules", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package qbittorrent_search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/torrent_clients/qbittorrent/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) Start(pattern string, plugins, categories []string) (int, error) {
|
||||
params := url.Values{}
|
||||
params.Add("pattern", pattern)
|
||||
params.Add("plugins", strings.Join(plugins, "|"))
|
||||
params.Add("category", strings.Join(categories, "|"))
|
||||
var res struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/start?"+params.Encode(), nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.ID, nil
|
||||
}
|
||||
|
||||
func (c Client) Stop(id int) error {
|
||||
params := url.Values{}
|
||||
params.Add("id", strconv.Itoa(id))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/stop?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetStatus(id int) (*qbittorrent_model.SearchStatus, error) {
|
||||
params := url.Values{}
|
||||
params.Add("id", strconv.Itoa(id))
|
||||
var res []*qbittorrent_model.SearchStatus
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/status?"+params.Encode(), nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res) < 1 {
|
||||
return nil, fmt.Errorf("response did not contain any statuses")
|
||||
}
|
||||
return res[0], nil
|
||||
}
|
||||
|
||||
func (c Client) GetStatuses() ([]*qbittorrent_model.SearchStatus, error) {
|
||||
var res []*qbittorrent_model.SearchStatus
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/status", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetResults(id, limit, offset int) (*qbittorrent_model.SearchResultsPaging, error) {
|
||||
params := url.Values{}
|
||||
params.Add("id", strconv.Itoa(id))
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
params.Add("offset", strconv.Itoa(offset))
|
||||
var res qbittorrent_model.SearchResultsPaging
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/results?"+params.Encode(), nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) Delete(id int) error {
|
||||
params := url.Values{}
|
||||
params.Add("id", strconv.Itoa(id))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/delete?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetCategories(plugins []string) ([]string, error) {
|
||||
endpoint := c.BaseUrl + "/categories"
|
||||
if plugins != nil {
|
||||
params := url.Values{}
|
||||
params.Add("pluginName", strings.Join(plugins, "|"))
|
||||
endpoint += "?" + params.Encode()
|
||||
}
|
||||
var res []string
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetPlugins() ([]qbittorrent_model.SearchPlugin, error) {
|
||||
var res []qbittorrent_model.SearchPlugin
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/plugins", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) InstallPlugins(sources []string) error {
|
||||
params := url.Values{}
|
||||
params.Add("sources", strings.Join(sources, "|"))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/installPlugin?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) UninstallPlugins(plugins []string) error {
|
||||
params := url.Values{}
|
||||
params.Add("names", strings.Join(plugins, "|"))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/uninstallPlugin?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) EnablePlugins(plugins []string, enable bool) error {
|
||||
params := url.Values{}
|
||||
params.Add("names", strings.Join(plugins, "|"))
|
||||
params.Add("enable", fmt.Sprintf("%v", enable))
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/enablePlugin?"+params.Encode(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) updatePlugins() error {
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/updatePlugins", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
90
seanime-2.9.10/internal/torrent_clients/qbittorrent/start.go
Normal file
90
seanime-2.9.10/internal/torrent_clients/qbittorrent/start.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"seanime/internal/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) getExecutableName() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "qbittorrent.exe"
|
||||
default:
|
||||
return "qbittorrent"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getExecutablePath() string {
|
||||
|
||||
if len(c.Path) > 0 {
|
||||
return c.Path
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "C:/Program Files/qBittorrent/qbittorrent.exe"
|
||||
case "linux":
|
||||
return "/usr/bin/qbittorrent" // Default path for Client on most Linux distributions
|
||||
case "darwin":
|
||||
return "/Applications/qbittorrent.app/Contents/MacOS/qbittorrent" // Default path for Client on macOS
|
||||
default:
|
||||
return "C:/Program Files/qBittorrent/qbittorrent.exe"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Start() error {
|
||||
|
||||
// If the path is empty, do not check if qBittorrent is running
|
||||
if c.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := c.getExecutableName()
|
||||
if util.ProgramIsRunning(name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
exe := c.getExecutablePath()
|
||||
cmd := util.NewCmd(exe)
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return errors.New("failed to start qBittorrent")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CheckStart() bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the path is empty, assume it's running
|
||||
if c.Path == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
_, err := c.Application.GetAppVersion()
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
err = c.Start()
|
||||
timeout := time.After(30 * time.Second)
|
||||
ticker := time.Tick(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
_, err = c.Application.GetAppVersion()
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
case <-timeout:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package qbittorrent
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_Start(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
|
||||
client := NewClient(&NewClientOptions{
|
||||
Logger: util.NewLogger(),
|
||||
Username: test_utils.ConfigData.Provider.QbittorrentUsername,
|
||||
Password: test_utils.ConfigData.Provider.QbittorrentPassword,
|
||||
Port: test_utils.ConfigData.Provider.QbittorrentPort,
|
||||
Host: test_utils.ConfigData.Provider.QbittorrentHost,
|
||||
Path: test_utils.ConfigData.Provider.QbittorrentPath,
|
||||
})
|
||||
|
||||
err := client.Start()
|
||||
assert.Nil(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestClient_CheckStart(t *testing.T) {
|
||||
|
||||
client := NewClient(&NewClientOptions{
|
||||
Logger: util.NewLogger(),
|
||||
Username: test_utils.ConfigData.Provider.QbittorrentUsername,
|
||||
Password: test_utils.ConfigData.Provider.QbittorrentPassword,
|
||||
Port: test_utils.ConfigData.Provider.QbittorrentPort,
|
||||
Host: test_utils.ConfigData.Provider.QbittorrentHost,
|
||||
Path: test_utils.ConfigData.Provider.QbittorrentPath,
|
||||
})
|
||||
|
||||
started := client.CheckStart()
|
||||
assert.True(t, started)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package qbittorrent_sync
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/torrent_clients/qbittorrent/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) GetMainData(rid int) (*qbittorrent_model.SyncMainData, error) {
|
||||
params := url.Values{}
|
||||
params.Add("rid", strconv.Itoa(rid))
|
||||
endpoint := c.BaseUrl + "/maindata?" + params.Encode()
|
||||
var res qbittorrent_model.SyncMainData
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetTorrentPeersData(hash string, rid int) (*qbittorrent_model.SyncPeersData, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("rid", strconv.Itoa(rid))
|
||||
endpoint := c.BaseUrl + "/torrentPeers?" + params.Encode()
|
||||
var res qbittorrent_model.SyncPeersData
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package qbittorrent_torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
qbittorrent_model "seanime/internal/torrent_clients/qbittorrent/model"
|
||||
qbittorrent_util "seanime/internal/torrent_clients/qbittorrent/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) GetList(options *qbittorrent_model.GetTorrentListOptions) ([]*qbittorrent_model.Torrent, error) {
|
||||
endpoint := c.BaseUrl + "/info"
|
||||
if options != nil {
|
||||
params, err := query.Values(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint += "?" + params.Encode()
|
||||
}
|
||||
var res []*qbittorrent_model.Torrent
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetProperties(hash string) (*qbittorrent_model.TorrentProperties, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/properties?" + params.Encode()
|
||||
var res qbittorrent_model.TorrentProperties
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetTrackers(hash string) ([]*qbittorrent_model.TorrentTracker, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/trackers?" + params.Encode()
|
||||
var res []*qbittorrent_model.TorrentTracker
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetWebSeeds(hash string) ([]string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/trackers?" + params.Encode()
|
||||
var res []struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var seeds []string
|
||||
for _, seed := range res {
|
||||
seeds = append(seeds, seed.URL)
|
||||
}
|
||||
return seeds, nil
|
||||
}
|
||||
|
||||
func (c Client) GetContents(hash string) ([]*qbittorrent_model.TorrentContent, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/files?" + params.Encode()
|
||||
var res []*qbittorrent_model.TorrentContent
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetPieceStates(hash string) ([]qbittorrent_model.TorrentPieceState, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/pieceStates?" + params.Encode()
|
||||
var res []qbittorrent_model.TorrentPieceState
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) GetPieceHashes(hash string) ([]string, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
endpoint := c.BaseUrl + "/pieceHashes?" + params.Encode()
|
||||
var res []string
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, endpoint, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) StopTorrents(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/pause", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/stop", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) ResumeTorrents(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/resume", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/start", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("deleteFiles", fmt.Sprintf("%v", deleteFiles))
|
||||
params.Add("hashes", value)
|
||||
//endpoint := c.BaseUrl + "/delete?" + params.Encode()
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/delete", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RecheckTorrents(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/recheck?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) ReannounceTorrents(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/reannounce?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) AddURLs(urls []string, options *qbittorrent_model.AddTorrentsOptions) error {
|
||||
if err := qbittorrent_util.PostMultipartLinks(c.Client, c.BaseUrl+"/add", options, urls); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) AddFiles(files map[string][]byte, options *qbittorrent_model.AddTorrentsOptions) error {
|
||||
if err := qbittorrent_util.PostMultipartFiles(c.Client, c.BaseUrl+"/add", options, files); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) AddTrackers(hash string, trackerURLs []string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("urls", strings.Join(trackerURLs, "\n"))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/addTrackers",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) EditTrackers(hash, old, new string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("origUrl", old)
|
||||
params.Add("newUrl", new)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/editTracker",
|
||||
strings.NewReader(params.Encode()),
|
||||
"application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RemoveTrackers(hash string, trackerURLs []string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("urls", strings.Join(trackerURLs, "|"))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/removeTrackers",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) IncreasePriority(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/increasePrio?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) DecreasePriority(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/decreasePrio?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetMaximumPriority(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/topPrio?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetMinimumPriority(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/bottomPrio?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetFilePriorities(hash string, ids []string, priority qbittorrent_model.TorrentPriority) error {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("id", strings.Join(ids, "|"))
|
||||
params.Add("priority", strconv.Itoa(int(priority)))
|
||||
//endpoint := c.BaseUrl + "/filePrio?" + params.Encode()
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/filePrio", strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetDownloadLimits(hashes []string) (map[string]int, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
var res map[string]int
|
||||
if err := qbittorrent_util.GetIntoWithContentType(c.Client, &res, c.BaseUrl+"/downloadLimit",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) SetDownloadLimits(hashes []string, limit int) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setDownloadLimit",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetShareLimits(hashes []string, ratioLimit float64, seedingTimeLimit int) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("ratioLimit", strconv.FormatFloat(ratioLimit, 'f', -1, 64))
|
||||
params.Add("seedingTimeLimit", strconv.Itoa(seedingTimeLimit))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setShareLimits",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetUploadLimits(hashes []string) (map[string]int, error) {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
var res map[string]int
|
||||
if err := qbittorrent_util.GetIntoWithContentType(c.Client, &res, c.BaseUrl+"/uploadLimit",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) SetUploadLimits(hashes []string, limit int) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setUploadLimit",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetLocations(hashes []string, location string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("location", location)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setLocation",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetName(hash string, name string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hash", hash)
|
||||
params.Add("name", name)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/rename",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetCategories(hashes []string, category string) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("category", category)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setCategory",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetCategories() (map[string]*qbittorrent_model.Category, error) {
|
||||
var res map[string]*qbittorrent_model.Category
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/categories", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) AddCategory(category string, savePath string) error {
|
||||
params := url.Values{}
|
||||
params.Add("category", category)
|
||||
params.Add("savePath", savePath)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/createCategory",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) EditCategory(category string, savePath string) error {
|
||||
params := url.Values{}
|
||||
params.Add("category", category)
|
||||
params.Add("savePath", savePath)
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/editCategory",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) RemoveCategory(categories []string) error {
|
||||
params := url.Values{}
|
||||
params.Add("categories", strings.Join(categories, "\n"))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/removeCategories",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetAutomaticManagement(hashes []string, enable bool) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("enable", fmt.Sprintf("%v", enable))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setAutoManagement",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) ToggleSequentialDownload(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/toggleSequentialDownload?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) ToggleFirstLastPiecePriority(hashes []string) error {
|
||||
value := strings.Join(hashes, "|")
|
||||
params := url.Values{}
|
||||
params.Add("hashes", value)
|
||||
endpoint := c.BaseUrl + "/toggleFirstLastPiecePrio?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetForceStart(hashes []string, enable bool) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("value", fmt.Sprintf("%v", enable))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setForceStart",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) SetSuperSeeding(hashes []string, enable bool) error {
|
||||
params := url.Values{}
|
||||
params.Add("hashes", strings.Join(hashes, "|"))
|
||||
params.Add("value", fmt.Sprintf("%v", enable))
|
||||
if err := qbittorrent_util.PostWithContentType(c.Client, c.BaseUrl+"/setSuperSeeding",
|
||||
strings.NewReader(params.Encode()), "application/x-www-form-urlencoded"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package qbittorrent_transfer
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/torrent_clients/qbittorrent/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseUrl string
|
||||
Client *http.Client
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func (c Client) GetTransferInfo() (*qbittorrent_model.TransferInfo, error) {
|
||||
var res qbittorrent_model.TransferInfo
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/info", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c Client) AlternativeSpeedLimitsEnabled() (bool, error) {
|
||||
var res int
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/speedLimitsMode", nil); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res == 1, nil
|
||||
}
|
||||
|
||||
func (c Client) ToggleAlternativeSpeedLimits() error {
|
||||
if err := qbittorrent_util.Post(c.Client, c.BaseUrl+"/toggleSpeedLimitsMode", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetGlobalDownloadLimit() (int, error) {
|
||||
var res int
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/downloadLimit", nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) SetGlobalDownloadLimit(limit int) error {
|
||||
params := url.Values{}
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
endpoint := c.BaseUrl + "/setDownloadLimit?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) GetGlobalUploadLimit() (int, error) {
|
||||
var res int
|
||||
if err := qbittorrent_util.GetInto(c.Client, &res, c.BaseUrl+"/uploadLimit", nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Client) SetGlobalUploadLimit(limit int) error {
|
||||
params := url.Values{}
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
endpoint := c.BaseUrl + "/setUploadLimit?" + params.Encode()
|
||||
if err := qbittorrent_util.Post(c.Client, endpoint, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
228
seanime-2.9.10/internal/torrent_clients/qbittorrent/util/util.go
Normal file
228
seanime-2.9.10/internal/torrent_clients/qbittorrent/util/util.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package qbittorrent_util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetInto(client *http.Client, target interface{}, url string, body interface{}) (err error) {
|
||||
var buffer bytes.Buffer
|
||||
if body != nil {
|
||||
if err := json.NewEncoder(&buffer).Encode(body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r, err := http.NewRequest("GET", url, &buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid response status %s", resp.Status)
|
||||
}
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.NewDecoder(bytes.NewReader(buf)).Decode(target); err != nil {
|
||||
if err2 := json.NewDecoder(strings.NewReader(`"` + string(buf) + `"`)).Decode(target); err2 != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Post(client *http.Client, url string, body interface{}) (err error) {
|
||||
var buffer bytes.Buffer
|
||||
if err := json.NewEncoder(&buffer).Encode(body); err != nil {
|
||||
return err
|
||||
}
|
||||
r, err := http.NewRequest("POST", url, &buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFormFileWithHeader(writer *multipart.Writer, name, filename string, headers map[string]string) (io.Writer, error) {
|
||||
header := textproto.MIMEHeader{}
|
||||
header.Add("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, name, filename))
|
||||
for key, value := range headers {
|
||||
header.Add(key, value)
|
||||
}
|
||||
return writer.CreatePart(header)
|
||||
}
|
||||
|
||||
func PostMultipartLinks(client *http.Client, url string, options *qbittorrent_model.AddTorrentsOptions, links []string) (err error) {
|
||||
var o map[string]interface{}
|
||||
if options != nil {
|
||||
b, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, &o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
form := multipart.NewWriter(&buf)
|
||||
if err := form.WriteField("urls", strings.Join(links, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range o {
|
||||
if err := form.WriteField(key, fmt.Sprintf("%v", value)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := form.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("content-type", "multipart/form-data; boundary="+form.Boundary())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostMultipartFiles(client *http.Client, url string, options *qbittorrent_model.AddTorrentsOptions, files map[string][]byte) (err error) {
|
||||
var o map[string]interface{}
|
||||
if options != nil {
|
||||
b, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, &o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
form := multipart.NewWriter(&buf)
|
||||
for filename, file := range files {
|
||||
writer, err := createFormFileWithHeader(form, "torrents", filename, map[string]string{
|
||||
"content-type": "application/x-bittorrent",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for key, value := range o {
|
||||
if err := form.WriteField(key, fmt.Sprintf("%v", value)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := form.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("content-type", "multipart/form-data; boundary="+form.Boundary())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostWithContentType(client *http.Client, url string, body io.Reader, contentType string) (err error) {
|
||||
r, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Add("content-type", contentType)
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid status %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetIntoWithContentType(client *http.Client, target interface{}, url string, body io.Reader, contentType string) (err error) {
|
||||
r, err := http.NewRequest("GET", url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Add("content-type", contentType)
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := resp.Body.Close(); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("invalid response status %s", resp.Status)
|
||||
}
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.NewDecoder(bytes.NewReader(buf)).Decode(target); err != nil {
|
||||
if err2 := json.NewDecoder(strings.NewReader(`"` + string(buf) + `"`)).Decode(target); err2 != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package torrent_client
|
||||
@@ -0,0 +1,420 @@
|
||||
package torrent_client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/torrent_clients/qbittorrent"
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/torrent_clients/transmission"
|
||||
"seanime/internal/torrents/torrent"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
QbittorrentClient = "qbittorrent"
|
||||
TransmissionClient = "transmission"
|
||||
NoneClient = "none"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository struct {
|
||||
logger *zerolog.Logger
|
||||
qBittorrentClient *qbittorrent.Client
|
||||
transmission *transmission.Transmission
|
||||
torrentRepository *torrent.Repository
|
||||
provider string
|
||||
metadataProvider metadata.Provider
|
||||
activeTorrentCountCtxCancel context.CancelFunc
|
||||
activeTorrentCount *ActiveCount
|
||||
}
|
||||
|
||||
NewRepositoryOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
QbittorrentClient *qbittorrent.Client
|
||||
Transmission *transmission.Transmission
|
||||
TorrentRepository *torrent.Repository
|
||||
Provider string
|
||||
MetadataProvider metadata.Provider
|
||||
}
|
||||
|
||||
ActiveCount struct {
|
||||
Downloading int `json:"downloading"`
|
||||
Seeding int `json:"seeding"`
|
||||
Paused int `json:"paused"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRepository(opts *NewRepositoryOptions) *Repository {
|
||||
if opts.Provider == "" {
|
||||
opts.Provider = QbittorrentClient
|
||||
}
|
||||
return &Repository{
|
||||
logger: opts.Logger,
|
||||
qBittorrentClient: opts.QbittorrentClient,
|
||||
transmission: opts.Transmission,
|
||||
torrentRepository: opts.TorrentRepository,
|
||||
provider: opts.Provider,
|
||||
metadataProvider: opts.MetadataProvider,
|
||||
activeTorrentCount: &ActiveCount{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) Shutdown() {
|
||||
if r.activeTorrentCountCtxCancel != nil {
|
||||
r.activeTorrentCountCtxCancel()
|
||||
r.activeTorrentCountCtxCancel = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) InitActiveTorrentCount(enabled bool, wsEventManager events.WSEventManagerInterface) {
|
||||
if r.activeTorrentCountCtxCancel != nil {
|
||||
r.activeTorrentCountCtxCancel()
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, r.activeTorrentCountCtxCancel = context.WithCancel(context.Background())
|
||||
go func(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Second * 5)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
r.GetActiveCount(r.activeTorrentCount)
|
||||
wsEventManager.SendEvent(events.ActiveTorrentCountUpdated, r.activeTorrentCount)
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (r *Repository) GetProvider() string {
|
||||
return r.provider
|
||||
}
|
||||
|
||||
func (r *Repository) Start() bool {
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
return r.qBittorrentClient.CheckStart()
|
||||
case TransmissionClient:
|
||||
return r.transmission.CheckStart()
|
||||
case NoneClient:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (r *Repository) TorrentExists(hash string) bool {
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
p, err := r.qBittorrentClient.Torrent.GetProperties(hash)
|
||||
return err == nil && p != nil
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGetAllForHashes(context.Background(), []string{hash})
|
||||
return err == nil && len(torrents) > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetList will return all torrents from the torrent client.
|
||||
func (r *Repository) GetList() ([]*Torrent, error) {
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
torrents, err := r.qBittorrentClient.Torrent.GetList(&qbittorrent_model.GetTorrentListOptions{Filter: "all"})
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while getting torrent list (qBittorrent)")
|
||||
return nil, err
|
||||
}
|
||||
return r.FromQbitTorrents(torrents), nil
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGetAll(context.Background())
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while getting torrent list (Transmission)")
|
||||
return nil, err
|
||||
}
|
||||
return r.FromTransmissionTorrents(torrents), nil
|
||||
default:
|
||||
return nil, errors.New("torrent client: No torrent client provider found")
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveCount will return the count of active torrents (downloading, seeding, paused).
|
||||
func (r *Repository) GetActiveCount(ret *ActiveCount) {
|
||||
ret.Seeding = 0
|
||||
ret.Downloading = 0
|
||||
ret.Paused = 0
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
torrents, err := r.qBittorrentClient.Torrent.GetList(&qbittorrent_model.GetTorrentListOptions{Filter: "downloading"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
torrents2, err := r.qBittorrentClient.Torrent.GetList(&qbittorrent_model.GetTorrentListOptions{Filter: "seeding"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
torrents = append(torrents, torrents2...)
|
||||
for _, t := range torrents {
|
||||
switch fromQbitTorrentStatus(t.State) {
|
||||
case TorrentStatusDownloading:
|
||||
ret.Downloading++
|
||||
case TorrentStatusSeeding:
|
||||
ret.Seeding++
|
||||
case TorrentStatusPaused:
|
||||
ret.Paused++
|
||||
}
|
||||
}
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGet(context.Background(), []string{"id", "status", "isFinished"}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range torrents {
|
||||
if t.Status == nil || t.IsFinished == nil {
|
||||
continue
|
||||
}
|
||||
switch fromTransmissionTorrentStatus(*t.Status, *t.IsFinished) {
|
||||
case TorrentStatusDownloading:
|
||||
ret.Downloading++
|
||||
case TorrentStatusSeeding:
|
||||
ret.Seeding++
|
||||
case TorrentStatusPaused:
|
||||
ret.Paused++
|
||||
}
|
||||
}
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveTorrents will return all torrents that are currently downloading, paused or seeding.
|
||||
func (r *Repository) GetActiveTorrents() ([]*Torrent, error) {
|
||||
torrents, err := r.GetList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var active []*Torrent
|
||||
for _, t := range torrents {
|
||||
if t.Status == TorrentStatusDownloading || t.Status == TorrentStatusSeeding || t.Status == TorrentStatusPaused {
|
||||
active = append(active, t)
|
||||
}
|
||||
}
|
||||
return active, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AddMagnets(magnets []string, dest string) error {
|
||||
r.logger.Trace().Any("magnets", magnets).Msg("torrent client: Adding magnets")
|
||||
|
||||
if len(magnets) == 0 {
|
||||
r.logger.Debug().Msg("torrent client: No magnets to add")
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
err = r.qBittorrentClient.Torrent.AddURLs(magnets, &qbittorrent_model.AddTorrentsOptions{
|
||||
Savepath: dest,
|
||||
Tags: r.qBittorrentClient.Tags,
|
||||
})
|
||||
case TransmissionClient:
|
||||
for _, magnet := range magnets {
|
||||
_, err = r.transmission.Client.TorrentAdd(context.Background(), transmissionrpc.TorrentAddPayload{
|
||||
Filename: &magnet,
|
||||
DownloadDir: &dest,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while adding magnets (Transmission)")
|
||||
break
|
||||
}
|
||||
}
|
||||
case NoneClient:
|
||||
return errors.New("torrent client: No torrent client selected")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while adding magnets")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Msg("torrent client: Added torrents")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) RemoveTorrents(hashes []string) error {
|
||||
r.logger.Trace().Msg("torrent client: Removing torrents")
|
||||
|
||||
var err error
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
err = r.qBittorrentClient.Torrent.DeleteTorrents(hashes, true)
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGetAllForHashes(context.Background(), hashes)
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while fetching torrents (Transmission)")
|
||||
return err
|
||||
}
|
||||
ids := make([]int64, len(torrents))
|
||||
for i, t := range torrents {
|
||||
ids[i] = *t.ID
|
||||
}
|
||||
err = r.transmission.Client.TorrentRemove(context.Background(), transmissionrpc.TorrentRemovePayload{
|
||||
IDs: ids,
|
||||
DeleteLocalData: true,
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while removing torrents (Transmission)")
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while removing torrents")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Any("hashes", hashes).Msg("torrent client: Removed torrents")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) PauseTorrents(hashes []string) error {
|
||||
r.logger.Trace().Msg("torrent client: Pausing torrents")
|
||||
|
||||
var err error
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
err = r.qBittorrentClient.Torrent.StopTorrents(hashes)
|
||||
case TransmissionClient:
|
||||
err = r.transmission.Client.TorrentStopHashes(context.Background(), hashes)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while pausing torrents")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Any("hashes", hashes).Msg("torrent client: Paused torrents")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ResumeTorrents(hashes []string) error {
|
||||
r.logger.Trace().Msg("torrent client: Resuming torrents")
|
||||
|
||||
var err error
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
err = r.qBittorrentClient.Torrent.ResumeTorrents(hashes)
|
||||
case TransmissionClient:
|
||||
err = r.transmission.Client.TorrentStartHashes(context.Background(), hashes)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while resuming torrents")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Any("hashes", hashes).Msg("torrent client: Resumed torrents")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeselectFiles(hash string, indices []int) error {
|
||||
|
||||
var err error
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
strIndices := make([]string, len(indices), len(indices))
|
||||
for i, v := range indices {
|
||||
strIndices[i] = strconv.Itoa(v)
|
||||
}
|
||||
err = r.qBittorrentClient.Torrent.SetFilePriorities(hash, strIndices, 0)
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGetAllForHashes(context.Background(), []string{hash})
|
||||
if err != nil || torrents[0].ID == nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while deselecting files (Transmission)")
|
||||
return err
|
||||
}
|
||||
id := *torrents[0].ID
|
||||
ind := make([]int64, len(indices), len(indices))
|
||||
for i, v := range indices {
|
||||
ind[i] = int64(v)
|
||||
}
|
||||
err = r.transmission.Client.TorrentSet(context.Background(), transmissionrpc.TorrentSetPayload{
|
||||
FilesUnwanted: ind,
|
||||
IDs: []int64{id},
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: Error while deselecting files")
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Debug().Str("hash", hash).Any("indices", indices).Msg("torrent client: Deselected torrent files")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFiles blocks until the files are retrieved, or until timeout.
|
||||
func (r *Repository) GetFiles(hash string) (filenames []string, err error) {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
filenames = make([]string, 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
r.logger.Debug().Str("hash", hash).Msg("torrent client: Getting torrent files")
|
||||
defer close(done)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = errors.New("torrent client: Unable to retrieve torrent files (timeout)")
|
||||
return
|
||||
case <-ticker.C:
|
||||
switch r.provider {
|
||||
case QbittorrentClient:
|
||||
qbitFiles, err := r.qBittorrentClient.Torrent.GetContents(hash)
|
||||
if err == nil && qbitFiles != nil && len(qbitFiles) > 0 {
|
||||
r.logger.Debug().Str("hash", hash).Int("count", len(qbitFiles)).Msg("torrent client: Retrieved torrent files")
|
||||
for _, f := range qbitFiles {
|
||||
filenames = append(filenames, f.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
case TransmissionClient:
|
||||
torrents, err := r.transmission.Client.TorrentGetAllForHashes(context.Background(), []string{hash})
|
||||
if err == nil && len(torrents) > 0 && torrents[0].Files != nil && len(torrents[0].Files) > 0 {
|
||||
transmissionFiles := torrents[0].Files
|
||||
r.logger.Debug().Str("hash", hash).Int("count", len(transmissionFiles)).Msg("torrent client: Retrieved torrent files")
|
||||
for _, f := range transmissionFiles {
|
||||
filenames = append(filenames, f.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-done // wait for the files to be retrieved
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package torrent_client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/torrents/analyzer"
|
||||
"time"
|
||||
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
)
|
||||
|
||||
type (
|
||||
SmartSelectParams struct {
|
||||
Torrent *hibiketorrent.AnimeTorrent
|
||||
EpisodeNumbers []int
|
||||
Media *anilist.CompleteAnime
|
||||
Destination string
|
||||
ShouldAddTorrent bool
|
||||
Platform platform.Platform
|
||||
}
|
||||
)
|
||||
|
||||
// SmartSelect will automatically the provided episode files from the torrent.
|
||||
// If the torrent has not been added yet, set SmartSelect.ShouldAddTorrent to true.
|
||||
// The torrent will NOT be removed if the selection fails.
|
||||
func (r *Repository) SmartSelect(p *SmartSelectParams) error {
|
||||
if p.Media == nil || p.Platform == nil || r.torrentRepository == nil {
|
||||
r.logger.Error().Msg("torrent client: media or platform is nil (smart select)")
|
||||
return errors.New("media or anilist client wrapper is nil")
|
||||
}
|
||||
|
||||
providerExtension, ok := r.torrentRepository.GetAnimeProviderExtension(p.Torrent.Provider)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", p.Torrent.Provider).Msg("torrent client: provider extension not found (smart select)")
|
||||
return errors.New("provider extension not found")
|
||||
}
|
||||
|
||||
if p.Media.IsMovieOrSingleEpisode() {
|
||||
return errors.New("smart select is not supported for movies or single-episode series")
|
||||
}
|
||||
|
||||
if len(p.EpisodeNumbers) == 0 {
|
||||
r.logger.Error().Msg("torrent client: no episode numbers provided (smart select)")
|
||||
return errors.New("no episode numbers provided")
|
||||
}
|
||||
|
||||
if p.ShouldAddTorrent {
|
||||
r.logger.Info().Msg("torrent client: adding torrent (smart select)")
|
||||
// Get magnet
|
||||
magnet, err := providerExtension.GetProvider().GetTorrentMagnetLink(p.Torrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add the torrent
|
||||
err = r.AddMagnets([]string{magnet}, p.Destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
filepaths, err := r.GetFiles(p.Torrent.InfoHash)
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: error getting files (smart select)")
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return fmt.Errorf("error getting files, torrent still added: %w", err)
|
||||
}
|
||||
|
||||
// Pause the torrent
|
||||
err = r.PauseTorrents([]string{p.Torrent.InfoHash})
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: error while pausing torrent (smart select)")
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return fmt.Errorf("error while selecting files: %w", err)
|
||||
}
|
||||
|
||||
// AnalyzeTorrentFiles the torrent files
|
||||
analyzer := torrent_analyzer.NewAnalyzer(&torrent_analyzer.NewAnalyzerOptions{
|
||||
Logger: r.logger,
|
||||
Filepaths: filepaths,
|
||||
Media: p.Media,
|
||||
Platform: p.Platform,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
})
|
||||
|
||||
r.logger.Debug().Msg("torrent client: analyzing torrent files (smart select)")
|
||||
|
||||
analysis, err := analyzer.AnalyzeTorrentFiles()
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: error while analyzing torrent files (smart select)")
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return fmt.Errorf("error while analyzing torrent files: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Debug().Msg("torrent client: finished analyzing torrent files (smart select)")
|
||||
|
||||
mainFiles := analysis.GetCorrespondingMainFiles()
|
||||
|
||||
// find episode number duplicates
|
||||
dup := make(map[int]int) // map[episodeNumber]count
|
||||
for _, f := range mainFiles {
|
||||
if _, ok := dup[f.GetLocalFile().GetEpisodeNumber()]; ok {
|
||||
dup[f.GetLocalFile().GetEpisodeNumber()]++
|
||||
} else {
|
||||
dup[f.GetLocalFile().GetEpisodeNumber()] = 1
|
||||
}
|
||||
}
|
||||
dupCount := 0
|
||||
for _, count := range dup {
|
||||
if count > 1 {
|
||||
dupCount++
|
||||
}
|
||||
}
|
||||
if dupCount > 2 {
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return errors.New("failed to select files, can't tell seasons apart")
|
||||
}
|
||||
|
||||
selectedFiles := make(map[int]*torrent_analyzer.File)
|
||||
selectedCount := 0
|
||||
for idx, f := range mainFiles {
|
||||
for _, ep := range p.EpisodeNumbers {
|
||||
if f.GetLocalFile().GetEpisodeNumber() == ep {
|
||||
selectedCount++
|
||||
selectedFiles[idx] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedCount == 0 || selectedCount < len(p.EpisodeNumbers) {
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return errors.New("failed to select files, could not find the right season files")
|
||||
}
|
||||
|
||||
indicesToRemove := analysis.GetUnselectedIndices(selectedFiles)
|
||||
|
||||
if len(indicesToRemove) > 0 {
|
||||
// Deselect files
|
||||
err = r.DeselectFiles(p.Torrent.InfoHash, indicesToRemove)
|
||||
if err != nil {
|
||||
r.logger.Err(err).Msg("torrent client: error while deselecting files (smart select)")
|
||||
_ = r.RemoveTorrents([]string{p.Torrent.InfoHash})
|
||||
return fmt.Errorf("error while deselecting files: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Resume the torrent
|
||||
_ = r.ResumeTorrents([]string{p.Torrent.InfoHash})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package torrent_client
|
||||
|
||||
//func TestSmartSelect(t *testing.T) {
|
||||
// t.Skip("Refactor test")
|
||||
// test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
//
|
||||
// _ = t.TempDir()
|
||||
//
|
||||
// anilistClient := anilist.TestGetMockAnilistClient()
|
||||
// _ = anilist_platform.NewAnilistPlatform(anilistClient, util.NewLogger())
|
||||
//
|
||||
// // get repo
|
||||
//
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// mediaId int
|
||||
// url string
|
||||
// selectedEpisodes []int
|
||||
// client string
|
||||
// }{
|
||||
// {
|
||||
// name: "Kakegurui xx (Season 2)",
|
||||
// mediaId: 100876,
|
||||
// url: "https://nyaa.si/view/1553978", // kakegurui season 1 + season 2
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12 in season 2
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family",
|
||||
// mediaId: 140960,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family Part 2",
|
||||
// mediaId: 142838,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12, 13}, // should select 22, 23, 24, 25
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Kakegurui xx (Season 2)",
|
||||
// mediaId: 100876,
|
||||
// url: "https://nyaa.si/view/1553978", // kakegurui season 1 + season 2
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12 in season 2
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family",
|
||||
// mediaId: 140960,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family Part 2",
|
||||
// mediaId: 142838,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12, 13}, // should select 22, 23, 24, 25
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// for _, tt := range tests {
|
||||
//
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
//
|
||||
// repo := getTestRepo(t, tt.client)
|
||||
//
|
||||
// ok := repo.Start()
|
||||
// if !assert.True(t, ok) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// })
|
||||
//
|
||||
// }
|
||||
//
|
||||
//}
|
||||
@@ -0,0 +1,164 @@
|
||||
package torrent_client
|
||||
|
||||
import (
|
||||
"seanime/internal/torrent_clients/qbittorrent/model"
|
||||
"seanime/internal/util"
|
||||
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
TorrentStatusDownloading TorrentStatus = "downloading"
|
||||
TorrentStatusSeeding TorrentStatus = "seeding"
|
||||
TorrentStatusPaused TorrentStatus = "paused"
|
||||
TorrentStatusOther TorrentStatus = "other"
|
||||
TorrentStatusStopped TorrentStatus = "stopped"
|
||||
)
|
||||
|
||||
type (
|
||||
Torrent struct {
|
||||
Name string `json:"name"`
|
||||
Hash string `json:"hash"`
|
||||
Seeds int `json:"seeds"`
|
||||
UpSpeed string `json:"upSpeed"`
|
||||
DownSpeed string `json:"downSpeed"`
|
||||
Progress float64 `json:"progress"`
|
||||
Size string `json:"size"`
|
||||
Eta string `json:"eta"`
|
||||
Status TorrentStatus `json:"status"`
|
||||
ContentPath string `json:"contentPath"`
|
||||
}
|
||||
TorrentStatus string
|
||||
)
|
||||
|
||||
//var torrentPool = util.NewPool[*Torrent](func() *Torrent {
|
||||
// return &Torrent{}
|
||||
//})
|
||||
|
||||
func (r *Repository) FromTransmissionTorrents(t []transmissionrpc.Torrent) []*Torrent {
|
||||
ret := make([]*Torrent, 0, len(t))
|
||||
for _, t := range t {
|
||||
ret = append(ret, r.FromTransmissionTorrent(&t))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *Repository) FromTransmissionTorrent(t *transmissionrpc.Torrent) *Torrent {
|
||||
torrent := &Torrent{}
|
||||
|
||||
torrent.Name = "N/A"
|
||||
if t.Name != nil {
|
||||
torrent.Name = *t.Name
|
||||
}
|
||||
|
||||
torrent.Hash = "N/A"
|
||||
if t.HashString != nil {
|
||||
torrent.Hash = *t.HashString
|
||||
}
|
||||
|
||||
torrent.Seeds = 0
|
||||
if t.PeersSendingToUs != nil {
|
||||
torrent.Seeds = int(*t.PeersSendingToUs)
|
||||
}
|
||||
|
||||
torrent.UpSpeed = "0 KB/s"
|
||||
if t.RateUpload != nil {
|
||||
torrent.UpSpeed = util.ToHumanReadableSpeed(int(*t.RateUpload))
|
||||
}
|
||||
|
||||
torrent.DownSpeed = "0 KB/s"
|
||||
if t.RateDownload != nil {
|
||||
torrent.DownSpeed = util.ToHumanReadableSpeed(int(*t.RateDownload))
|
||||
}
|
||||
|
||||
torrent.Progress = 0.0
|
||||
if t.PercentDone != nil {
|
||||
torrent.Progress = *t.PercentDone
|
||||
}
|
||||
|
||||
torrent.Size = "N/A"
|
||||
if t.TotalSize != nil {
|
||||
torrent.Size = util.Bytes(uint64(*t.TotalSize))
|
||||
}
|
||||
|
||||
torrent.Eta = "???"
|
||||
if t.ETA != nil {
|
||||
torrent.Eta = util.FormatETA(int(*t.ETA))
|
||||
}
|
||||
|
||||
torrent.ContentPath = ""
|
||||
if t.DownloadDir != nil {
|
||||
torrent.ContentPath = *t.DownloadDir
|
||||
}
|
||||
|
||||
torrent.Status = TorrentStatusOther
|
||||
if t.Status != nil && t.IsFinished != nil {
|
||||
torrent.Status = fromTransmissionTorrentStatus(*t.Status, *t.IsFinished)
|
||||
}
|
||||
|
||||
return torrent
|
||||
}
|
||||
|
||||
// fromTransmissionTorrentStatus returns a normalized status for the torrent.
|
||||
func fromTransmissionTorrentStatus(st transmissionrpc.TorrentStatus, isFinished bool) TorrentStatus {
|
||||
if st == transmissionrpc.TorrentStatusSeed || st == transmissionrpc.TorrentStatusSeedWait {
|
||||
return TorrentStatusSeeding
|
||||
} else if st == transmissionrpc.TorrentStatusStopped && isFinished {
|
||||
return TorrentStatusStopped
|
||||
} else if st == transmissionrpc.TorrentStatusStopped && !isFinished {
|
||||
return TorrentStatusPaused
|
||||
} else if st == transmissionrpc.TorrentStatusDownload || st == transmissionrpc.TorrentStatusDownloadWait {
|
||||
return TorrentStatusDownloading
|
||||
} else {
|
||||
return TorrentStatusOther
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) FromQbitTorrents(t []*qbittorrent_model.Torrent) []*Torrent {
|
||||
ret := make([]*Torrent, 0, len(t))
|
||||
for _, t := range t {
|
||||
ret = append(ret, r.FromQbitTorrent(t))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
func (r *Repository) FromQbitTorrent(t *qbittorrent_model.Torrent) *Torrent {
|
||||
torrent := &Torrent{}
|
||||
|
||||
torrent.Name = t.Name
|
||||
torrent.Hash = t.Hash
|
||||
torrent.Seeds = t.NumSeeds
|
||||
torrent.UpSpeed = util.ToHumanReadableSpeed(t.Upspeed)
|
||||
torrent.DownSpeed = util.ToHumanReadableSpeed(t.Dlspeed)
|
||||
torrent.Progress = t.Progress
|
||||
torrent.Size = util.Bytes(uint64(t.Size))
|
||||
torrent.Eta = util.FormatETA(t.Eta)
|
||||
torrent.ContentPath = t.ContentPath
|
||||
torrent.Status = fromQbitTorrentStatus(t.State)
|
||||
|
||||
return torrent
|
||||
}
|
||||
|
||||
// fromQbitTorrentStatus returns a normalized status for the torrent.
|
||||
func fromQbitTorrentStatus(st qbittorrent_model.TorrentState) TorrentStatus {
|
||||
if st == qbittorrent_model.StateQueuedUP ||
|
||||
st == qbittorrent_model.StateStalledUP ||
|
||||
st == qbittorrent_model.StateForcedUP ||
|
||||
st == qbittorrent_model.StateCheckingUP ||
|
||||
st == qbittorrent_model.StateUploading {
|
||||
return TorrentStatusSeeding
|
||||
} else if st == qbittorrent_model.StatePausedDL || st == qbittorrent_model.StateStoppedDL {
|
||||
return TorrentStatusPaused
|
||||
} else if st == qbittorrent_model.StateDownloading ||
|
||||
st == qbittorrent_model.StateCheckingDL ||
|
||||
st == qbittorrent_model.StateStalledDL ||
|
||||
st == qbittorrent_model.StateQueuedDL ||
|
||||
st == qbittorrent_model.StateMetaDL ||
|
||||
st == qbittorrent_model.StateAllocating ||
|
||||
st == qbittorrent_model.StateForceDL {
|
||||
return TorrentStatusDownloading
|
||||
} else if st == qbittorrent_model.StatePausedUP || st == qbittorrent_model.StateStoppedUP {
|
||||
return TorrentStatusStopped
|
||||
} else {
|
||||
return TorrentStatusOther
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package transmission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"runtime"
|
||||
"seanime/internal/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Transmission) getExecutableName() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "transmission-qt.exe"
|
||||
default:
|
||||
return "transmission-qt"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Transmission) getExecutablePath() string {
|
||||
|
||||
if len(c.Path) > 0 {
|
||||
return c.Path
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "C:/Program Files/Transmission/transmission-qt.exe"
|
||||
case "linux":
|
||||
return "/usr/bin/transmission-qt" // Default path for Transmission on most Linux distributions
|
||||
case "darwin":
|
||||
return "/Applications/Transmission.app/Contents/MacOS/transmission-qt"
|
||||
// Default path for Transmission on macOS
|
||||
default:
|
||||
return "C:/Program Files/Transmission/transmission-qt.exe"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Transmission) Start() error {
|
||||
|
||||
// If the path is empty, do not check if Transmission is running
|
||||
if c.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := c.getExecutableName()
|
||||
if util.ProgramIsRunning(name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
exe := c.getExecutablePath()
|
||||
cmd := util.NewCmd(exe)
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return errors.New("failed to start Transmission")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Transmission) CheckStart() bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the path is empty, assume it's running
|
||||
if c.Path == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
_, _, _, err := c.Client.RPCVersion(context.Background())
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
err = c.Start()
|
||||
timeout := time.After(30 * time.Second)
|
||||
ticker := time.Tick(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
_, _, _, err := c.Client.RPCVersion(context.Background())
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
case <-timeout:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package transmission
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Transmission struct {
|
||||
Client *transmissionrpc.Client
|
||||
Path string
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
NewTransmissionOptions struct {
|
||||
Path string
|
||||
Logger *zerolog.Logger
|
||||
Username string
|
||||
Password string
|
||||
Host string // Default: 127.0.0.1
|
||||
Port int
|
||||
}
|
||||
)
|
||||
|
||||
func New(options *NewTransmissionOptions) (*Transmission, error) {
|
||||
// Set default host
|
||||
if options.Host == "" {
|
||||
options.Host = "127.0.0.1"
|
||||
}
|
||||
|
||||
baseUrl := fmt.Sprintf("http://%s:%s@%s:%d/transmission/rpc",
|
||||
options.Username,
|
||||
url.QueryEscape(options.Password),
|
||||
options.Host,
|
||||
options.Port,
|
||||
)
|
||||
|
||||
if strings.HasPrefix(options.Host, "https://") {
|
||||
options.Host = strings.TrimPrefix(options.Host, "https://")
|
||||
baseUrl = fmt.Sprintf("https://%s:%s@%s:%d/transmission/rpc",
|
||||
options.Username,
|
||||
url.QueryEscape(options.Password),
|
||||
options.Host,
|
||||
options.Port,
|
||||
)
|
||||
}
|
||||
|
||||
_url, err := url.Parse(baseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, _ := transmissionrpc.New(_url, nil)
|
||||
return &Transmission{
|
||||
Client: client,
|
||||
Path: options.Path,
|
||||
Logger: options.Logger,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package transmission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
//func TestGetActiveTorrents(t *testing.T) {
|
||||
// t.Skip("Provide magnets")
|
||||
// test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
//
|
||||
// trans, err := New(&NewTransmissionOptions{
|
||||
// Host: test_utils.ConfigData.Provider.TransmissionHost,
|
||||
// Path: test_utils.ConfigData.Provider.TransmissionPath,
|
||||
// Port: test_utils.ConfigData.Provider.TransmissionPort,
|
||||
// Username: test_utils.ConfigData.Provider.TransmissionUsername,
|
||||
// Password: test_utils.ConfigData.Provider.TransmissionPassword,
|
||||
// Logger: util.NewLogger(),
|
||||
// })
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
func TestGetFiles(t *testing.T) {
|
||||
t.Skip("Provide magnets")
|
||||
test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
magnet string
|
||||
mediaId int
|
||||
expectedNbFiles int
|
||||
}{
|
||||
{
|
||||
name: "[EMBER] Demon Slayer (2023) (Season 3)",
|
||||
url: "https://animetosho.org/view/ember-demon-slayer-2023-season-3-bdrip-1080p.n1778316",
|
||||
magnet: "",
|
||||
mediaId: 145139,
|
||||
expectedNbFiles: 11,
|
||||
},
|
||||
{
|
||||
name: "[Tenrai-Sensei] Kakegurui (Season 1-2 + OVAs)",
|
||||
url: "https://nyaa.si/view/1553978",
|
||||
magnet: "",
|
||||
mediaId: 98314,
|
||||
expectedNbFiles: 27,
|
||||
},
|
||||
}
|
||||
|
||||
trans, err := New(&NewTransmissionOptions{
|
||||
Host: test_utils.ConfigData.Provider.TransmissionHost,
|
||||
Path: test_utils.ConfigData.Provider.TransmissionPath,
|
||||
Port: test_utils.ConfigData.Provider.TransmissionPort,
|
||||
Username: test_utils.ConfigData.Provider.TransmissionUsername,
|
||||
Password: test_utils.ConfigData.Provider.TransmissionPassword,
|
||||
Logger: util.NewLogger(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
to, err := trans.Client.TorrentAdd(context.Background(), transmissionrpc.TorrentAddPayload{
|
||||
Filename: &tt.magnet,
|
||||
DownloadDir: &tempDir,
|
||||
})
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
|
||||
time.Sleep(20 * time.Second)
|
||||
|
||||
// Get files
|
||||
torrents, err := trans.Client.TorrentGetAllFor(context.Background(), []int64{*to.ID})
|
||||
to = torrents[0]
|
||||
|
||||
spew.Dump(to.Files)
|
||||
|
||||
// Remove torrent
|
||||
err = trans.Client.TorrentRemove(context.Background(), transmissionrpc.TorrentRemovePayload{
|
||||
IDs: []int64{*to.ID},
|
||||
DeleteLocalData: true,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user