small refactor*

This commit is contained in:
royalcat 2025-03-22 08:49:14 +04:00
parent b6b541e050
commit 24a4d30275
232 changed files with 2164 additions and 1906 deletions

View file

@ -0,0 +1,396 @@
package qbittorrent
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
type Application interface {
// Version get application version
Version(context.Context) (string, error)
// WebApiVersion get webapi version
WebApiVersion(context.Context) (string, error)
// BuildInfo get build info
BuildInfo(context.Context) (*BuildInfo, error)
// Shutdown exit application
Shutdown(context.Context) error
// GetPreferences get application preferences
GetPreferences(context.Context) (*Preferences, error)
// SetPreferences set application preferences
SetPreferences(context.Context, *Preferences) error
// DefaultSavePath get default save path
DefaultSavePath(context.Context) (string, error)
}
type BuildInfo struct {
BitNess int `json:"bitness,omitempty"`
Boost string `json:"boost,omitempty"`
LibTorrent string `json:"libtorrent,omitempty"`
Openssl string `json:"openssl,omitempty"`
QT string `json:"qt,omitempty"`
Zlib string `json:"zlib,omitempty"`
}
type Preferences struct {
AddToTopOfQueue bool `json:"add_to_top_of_queue,omitempty"`
AddTrackers string `json:"add_trackers,omitempty"`
AddTrackersEnabled bool `json:"add_trackers_enabled,omitempty"`
AltDlLimit int `json:"alt_dl_limit,omitempty"`
AltUpLimit int `json:"alt_up_limit,omitempty"`
AlternativeWebuiEnabled bool `json:"alternative_webui_enabled,omitempty"`
AlternativeWebuiPath string `json:"alternative_webui_path,omitempty"`
AnnounceIP string `json:"announce_ip,omitempty"`
AnnounceToAllTiers bool `json:"announce_to_all_tiers,omitempty"`
AnnounceToAllTrackers bool `json:"announce_to_all_trackers,omitempty"`
AnonymousMode bool `json:"anonymous_mode,omitempty"`
AsyncIoThreads int `json:"async_io_threads,omitempty"`
AutoDeleteMode int `json:"auto_delete_mode,omitempty"`
AutoTmmEnabled bool `json:"auto_tmm_enabled,omitempty"`
AutorunEnabled bool `json:"autorun_enabled,omitempty"`
AutorunOnTorrentAddedEnabled bool `json:"autorun_on_torrent_added_enabled,omitempty"`
AutorunOnTorrentAddedProgram string `json:"autorun_on_torrent_added_program,omitempty"`
AutorunProgram string `json:"autorun_program,omitempty"`
BannedIPs string `json:"banned_IPs,omitempty"`
BdecodeDepthLimit int `json:"bdecode_depth_limit,omitempty"`
BdecodeTokenLimit int `json:"bdecode_token_limit,omitempty"`
BittorrentProtocol int `json:"bittorrent_protocol,omitempty"`
BlockPeersOnPrivilegedPorts bool `json:"block_peers_on_privileged_ports,omitempty"`
BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist,omitempty"`
BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled,omitempty"`
BypassLocalAuth bool `json:"bypass_local_auth,omitempty"`
CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled,omitempty"`
CheckingMemoryUse int `json:"checking_memory_use,omitempty"`
ConnectionSpeed int `json:"connection_speed,omitempty"`
CurrentInterfaceAddress string `json:"current_interface_address,omitempty"`
CurrentInterfaceName string `json:"current_interface_name,omitempty"`
CurrentNetworkInterface string `json:"current_network_interface,omitempty"`
Dht bool `json:"dht,omitempty"`
DiskCache int `json:"disk_cache,omitempty"`
DiskCacheTTL int `json:"disk_cache_ttl,omitempty"`
DiskIoReadMode int `json:"disk_io_read_mode,omitempty"`
DiskIoType int `json:"disk_io_type,omitempty"`
DiskIoWriteMode int `json:"disk_io_write_mode,omitempty"`
DiskQueueSize int `json:"disk_queue_size,omitempty"`
DlLimit int `json:"dl_limit,omitempty"`
DontCountSlowTorrents bool `json:"dont_count_slow_torrents,omitempty"`
DyndnsDomain string `json:"dyndns_domain,omitempty"`
DyndnsEnabled bool `json:"dyndns_enabled,omitempty"`
DyndnsPassword string `json:"dyndns_password,omitempty"`
DyndnsService int `json:"dyndns_service,omitempty"`
DyndnsUsername string `json:"dyndns_username,omitempty"`
EmbeddedTrackerPort int `json:"embedded_tracker_port,omitempty"`
EmbeddedTrackerPortForwarding bool `json:"embedded_tracker_port_forwarding,omitempty"`
EnableCoalesceReadWrite bool `json:"enable_coalesce_read_write,omitempty"`
EnableEmbeddedTracker bool `json:"enable_embedded_tracker,omitempty"`
EnableMultiConnectionsFromSameIP bool `json:"enable_multi_connections_from_same_ip,omitempty"`
EnablePieceExtentAffinity bool `json:"enable_piece_extent_affinity,omitempty"`
EnableUploadSuggestions bool `json:"enable_upload_suggestions,omitempty"`
Encryption int `json:"encryption,omitempty"`
ExcludedFileNames string `json:"excluded_file_names,omitempty"`
ExcludedFileNamesEnabled bool `json:"excluded_file_names_enabled,omitempty"`
ExportDir string `json:"export_dir,omitempty"`
ExportDirFin string `json:"export_dir_fin,omitempty"`
FileLogAge int `json:"file_log_age,omitempty"`
FileLogAgeType int `json:"file_log_age_type,omitempty"`
FileLogBackupEnabled bool `json:"file_log_backup_enabled,omitempty"`
FileLogDeleteOld bool `json:"file_log_delete_old,omitempty"`
FileLogEnabled bool `json:"file_log_enabled,omitempty"`
FileLogMaxSize int `json:"file_log_max_size,omitempty"`
FileLogPath string `json:"file_log_path,omitempty"`
FilePoolSize int `json:"file_pool_size,omitempty"`
HashingThreads int `json:"hashing_threads,omitempty"`
I2PAddress string `json:"i2p_address,omitempty"`
I2PEnabled bool `json:"i2p_enabled,omitempty"`
I2PInboundLength int `json:"i2p_inbound_length,omitempty"`
I2PInboundQuantity int `json:"i2p_inbound_quantity,omitempty"`
I2PMixedMode bool `json:"i2p_mixed_mode,omitempty"`
I2POutboundLength int `json:"i2p_outbound_length,omitempty"`
I2POutboundQuantity int `json:"i2p_outbound_quantity,omitempty"`
I2PPort int `json:"i2p_port,omitempty"`
IdnSupportEnabled bool `json:"idn_support_enabled,omitempty"`
IncompleteFilesExt bool `json:"incomplete_files_ext,omitempty"`
IPFilterEnabled bool `json:"ip_filter_enabled,omitempty"`
IPFilterPath string `json:"ip_filter_path,omitempty"`
IPFilterTrackers bool `json:"ip_filter_trackers,omitempty"`
LimitLanPeers bool `json:"limit_lan_peers,omitempty"`
LimitTCPOverhead bool `json:"limit_tcp_overhead,omitempty"`
LimitUtpRate bool `json:"limit_utp_rate,omitempty"`
ListenPort int `json:"listen_port,omitempty"`
Locale string `json:"locale,omitempty"`
Lsd bool `json:"lsd,omitempty"`
MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled,omitempty"`
MailNotificationEmail string `json:"mail_notification_email,omitempty"`
MailNotificationEnabled bool `json:"mail_notification_enabled,omitempty"`
MailNotificationPassword string `json:"mail_notification_password,omitempty"`
MailNotificationSender string `json:"mail_notification_sender,omitempty"`
MailNotificationSMTP string `json:"mail_notification_smtp,omitempty"`
MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled,omitempty"`
MailNotificationUsername string `json:"mail_notification_username,omitempty"`
MaxActiveCheckingTorrents int `json:"max_active_checking_torrents,omitempty"`
MaxActiveDownloads int `json:"max_active_downloads,omitempty"`
MaxActiveTorrents int `json:"max_active_torrents,omitempty"`
MaxActiveUploads int `json:"max_active_uploads,omitempty"`
MaxConcurrentHTTPAnnounces int `json:"max_concurrent_http_announces,omitempty"`
MaxConnec int `json:"max_connec,omitempty"`
MaxConnecPerTorrent int `json:"max_connec_per_torrent,omitempty"`
MaxInactiveSeedingTime int `json:"max_inactive_seeding_time,omitempty"`
MaxInactiveSeedingTimeEnabled bool `json:"max_inactive_seeding_time_enabled,omitempty"`
MaxRatio int `json:"max_ratio,omitempty"`
MaxRatioAct int `json:"max_ratio_act,omitempty"`
MaxRatioEnabled bool `json:"max_ratio_enabled,omitempty"`
MaxSeedingTime int `json:"max_seeding_time,omitempty"`
MaxSeedingTimeEnabled bool `json:"max_seeding_time_enabled,omitempty"`
MaxUploads int `json:"max_uploads,omitempty"`
MaxUploadsPerTorrent int `json:"max_uploads_per_torrent,omitempty"`
MemoryWorkingSetLimit int `json:"memory_working_set_limit,omitempty"`
MergeTrackers bool `json:"merge_trackers,omitempty"`
OutgoingPortsMax int `json:"outgoing_ports_max,omitempty"`
OutgoingPortsMin int `json:"outgoing_ports_min,omitempty"`
PeerTos int `json:"peer_tos,omitempty"`
PeerTurnover int `json:"peer_turnover,omitempty"`
PeerTurnoverCutoff int `json:"peer_turnover_cutoff,omitempty"`
PeerTurnoverInterval int `json:"peer_turnover_interval,omitempty"`
PerformanceWarning bool `json:"performance_warning,omitempty"`
Pex bool `json:"pex,omitempty"`
PreallocateAll bool `json:"preallocate_all,omitempty"`
ProxyAuthEnabled bool `json:"proxy_auth_enabled,omitempty"`
ProxyBittorrent bool `json:"proxy_bittorrent,omitempty"`
ProxyHostnameLookup bool `json:"proxy_hostname_lookup,omitempty"`
ProxyIP string `json:"proxy_ip,omitempty"`
ProxyMisc bool `json:"proxy_misc,omitempty"`
ProxyPassword string `json:"proxy_password,omitempty"`
ProxyPeerConnections bool `json:"proxy_peer_connections,omitempty"`
ProxyPort int `json:"proxy_port,omitempty"`
ProxyRss bool `json:"proxy_rss,omitempty"`
ProxyType string `json:"proxy_type,omitempty"`
ProxyUsername string `json:"proxy_username,omitempty"`
QueueingEnabled bool `json:"queueing_enabled,omitempty"`
RandomPort bool `json:"random_port,omitempty"`
ReannounceWhenAddressChanged bool `json:"reannounce_when_address_changed,omitempty"`
RecheckCompletedTorrents bool `json:"recheck_completed_torrents,omitempty"`
RefreshInterval int `json:"refresh_interval,omitempty"`
RequestQueueSize int `json:"request_queue_size,omitempty"`
ResolvePeerCountries bool `json:"resolve_peer_countries,omitempty"`
ResumeDataStorageType string `json:"resume_data_storage_type,omitempty"`
RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled,omitempty"`
RssDownloadRepackProperEpisodes bool `json:"rss_download_repack_proper_episodes,omitempty"`
RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed,omitempty"`
RssProcessingEnabled bool `json:"rss_processing_enabled,omitempty"`
RssRefreshInterval int `json:"rss_refresh_interval,omitempty"`
RssSmartEpisodeFilters string `json:"rss_smart_episode_filters,omitempty"`
SavePath string `json:"save_path,omitempty"`
SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled,omitempty"`
SaveResumeDataInterval int `json:"save_resume_data_interval,omitempty"`
ScheduleFromHour int `json:"schedule_from_hour,omitempty"`
ScheduleFromMin int `json:"schedule_from_min,omitempty"`
ScheduleToHour int `json:"schedule_to_hour,omitempty"`
ScheduleToMin int `json:"schedule_to_min,omitempty"`
SchedulerDays int `json:"scheduler_days,omitempty"`
SchedulerEnabled bool `json:"scheduler_enabled,omitempty"`
SendBufferLowWatermark int `json:"send_buffer_low_watermark,omitempty"`
SendBufferWatermark int `json:"send_buffer_watermark,omitempty"`
SendBufferWatermarkFactor int `json:"send_buffer_watermark_factor,omitempty"`
SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold,omitempty"`
SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer,omitempty"`
SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold,omitempty"`
SocketBacklogSize int `json:"socket_backlog_size,omitempty"`
SocketReceiveBufferSize int `json:"socket_receive_buffer_size,omitempty"`
SocketSendBufferSize int `json:"socket_send_buffer_size,omitempty"`
SsrfMitigation bool `json:"ssrf_mitigation,omitempty"`
StartPausedEnabled bool `json:"start_paused_enabled,omitempty"`
StopTrackerTimeout int `json:"stop_tracker_timeout,omitempty"`
TempPath string `json:"temp_path,omitempty"`
TempPathEnabled bool `json:"temp_path_enabled,omitempty"`
TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled,omitempty"`
TorrentContentLayout string `json:"torrent_content_layout,omitempty"`
TorrentFileSizeLimit int `json:"torrent_file_size_limit,omitempty"`
TorrentStopCondition string `json:"torrent_stop_condition,omitempty"`
UpLimit int `json:"up_limit,omitempty"`
UploadChokingAlgorithm int `json:"upload_choking_algorithm,omitempty"`
UploadSlotsBehavior int `json:"upload_slots_behavior,omitempty"`
Upnp bool `json:"upnp,omitempty"`
UpnpLeaseDuration int `json:"upnp_lease_duration,omitempty"`
UseCategoryPathsInManualMode bool `json:"use_category_paths_in_manual_mode,omitempty"`
UseHTTPS bool `json:"use_https,omitempty"`
UseSubcategories bool `json:"use_subcategories,omitempty"`
UtpTCPMixedMode int `json:"utp_tcp_mixed_mode,omitempty"`
ValidateHTTPSTrackerCertificate bool `json:"validate_https_tracker_certificate,omitempty"`
WebUIAddress string `json:"web_ui_address,omitempty"`
WebUIBanDuration int `json:"web_ui_ban_duration,omitempty"`
WebUIClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled,omitempty"`
WebUICsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled,omitempty"`
WebUICustomHTTPHeaders string `json:"web_ui_custom_http_headers,omitempty"`
WebUIDomainList string `json:"web_ui_domain_list,omitempty"`
WebUIHostHeaderValidationEnabled bool `json:"web_ui_host_header_validation_enabled,omitempty"`
WebUIHTTPSCertPath string `json:"web_ui_https_cert_path,omitempty"`
WebUIHTTPSKeyPath string `json:"web_ui_https_key_path,omitempty"`
WebUIMaxAuthFailCount int `json:"web_ui_max_auth_fail_count,omitempty"`
WebUIPort int `json:"web_ui_port,omitempty"`
WebUIReverseProxiesList string `json:"web_ui_reverse_proxies_list,omitempty"`
WebUIReverseProxyEnabled bool `json:"web_ui_reverse_proxy_enabled,omitempty"`
WebUISecureCookieEnabled bool `json:"web_ui_secure_cookie_enabled,omitempty"`
WebUISessionTimeout int `json:"web_ui_session_timeout,omitempty"`
WebUIUpnp bool `json:"web_ui_upnp,omitempty"`
WebUIUseCustomHTTPHeadersEnabled bool `json:"web_ui_use_custom_http_headers_enabled,omitempty"`
WebUIUsername string `json:"web_ui_username,omitempty"`
}
func (c *client) Version(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Application.Version")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/version", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("get version failed: " + string(result.body))
}
return string(result.body), nil
}
func (c *client) WebApiVersion(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Application.WebApiVersion")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/webapiVersion", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("get version failed: " + string(result.body))
}
return string(result.body), nil
}
func (c *client) BuildInfo(ctx context.Context) (*BuildInfo, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Application.BuildInfo")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/buildInfo", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get build info failed: " + string(result.body))
}
var build = new(BuildInfo)
if err := json.Unmarshal(result.body, build); err != nil {
return nil, err
}
return build, nil
}
func (c *client) Shutdown(ctx context.Context) error {
ctx, span := trace.Start(ctx, "qbittorrent.Application.Shutdown")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/shutdown", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
method: http.MethodPost,
url: apiUrl,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("shutdown application failed: " + string(result.body))
}
return nil
}
func (c *client) GetPreferences(ctx context.Context) (*Preferences, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Application.GetPreferences")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/preferences", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get preference failed: " + string(result.body))
}
var preferences = new(Preferences)
if err := json.Unmarshal(result.body, preferences); err != nil {
return nil, err
}
return preferences, nil
}
func (c *client) SetPreferences(ctx context.Context, prefs *Preferences) error {
ctx, span := trace.Start(ctx, "qbittorrent.Application.SetPreferences")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/setPreferences", c.config.Address)
data, err := json.Marshal(prefs)
if err != nil {
return err
}
var formData bytes.Buffer
formData.Write([]byte("json="))
formData.Write(data)
result, err := c.doRequest(ctx, &requestData{
method: http.MethodPost,
url: apiUrl,
contentType: ContentTypeFormUrlEncoded,
body: &formData,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("set preference failed: " + string(result.body))
}
return nil
}
func (c *client) DefaultSavePath(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Application.DefaultSavePath")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/app/defaultSavePath", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("get default save path failed: " + string(result.body))
}
return string(result.body), nil
}

View file

@ -0,0 +1,73 @@
package qbittorrent
import (
"context"
"testing"
)
func TestClient_Version(t *testing.T) {
ctx := context.Background()
version, err := c.Application().Version(ctx)
if err != nil {
t.Fatal(err)
}
t.Log(version)
}
func TestClient_WebApiVersion(t *testing.T) {
ctx := context.Background()
version, err := c.Application().WebApiVersion(ctx)
if err != nil {
t.Fatal(err)
}
t.Log(version)
}
func TestClient_BuildInfo(t *testing.T) {
ctx := context.Background()
info, err := c.Application().BuildInfo(ctx)
if err != nil {
t.Fatal(err)
}
t.Logf("build: %+v", info)
}
func TestClient_Shutdown(t *testing.T) {
ctx := context.Background()
if err := c.Application().Shutdown(ctx); err != nil {
t.Fatal(err)
}
t.Log("shutting down")
}
func TestClient_GetPreferences(t *testing.T) {
ctx := context.Background()
prefs, err := c.Application().GetPreferences(ctx)
if err != nil {
t.Fatal(err)
}
t.Logf("prefs: %+v", prefs)
}
func TestClient_SetPreferences(t *testing.T) {
ctx := context.Background()
prefs, err := c.Application().GetPreferences(ctx)
if err != nil {
t.Fatal(err)
}
prefs.FileLogAge = 301
if err := c.Application().SetPreferences(ctx, prefs); err != nil {
t.Fatal(err)
}
t.Logf("success")
}
func TestClient_DefaultSavePath(t *testing.T) {
ctx := context.Background()
path, err := c.Application().DefaultSavePath(ctx)
if err != nil {
t.Fatal(err)
}
t.Logf("path: %s", path)
}

View file

@ -0,0 +1,91 @@
package qbittorrent
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
)
type Authentication interface {
// Login cookie-based authentication, after calling NewClient, do not need to call Login again,
// it is the default behavior
Login(ctx context.Context) error
// Logout deactivate cookies
Logout(ctx context.Context) error
}
func (c *client) Login(ctx context.Context) error {
ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Login")
defer span.End()
if c.config.Username == "" || c.config.Password == "" {
return errors.New("username or password is empty")
}
formData := url.Values{}
formData.Set("username", c.config.Username)
formData.Set("password", c.config.Password)
encodedFormData := formData.Encode()
apiUrl := fmt.Sprintf("%s/api/v2/auth/login", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
method: http.MethodPost,
url: apiUrl,
body: strings.NewReader(encodedFormData),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("login failed: " + string(result.body))
}
if string(result.body) == "Fails." {
return ErrAuthFailed
}
if string(result.body) != "Ok." {
return errors.New("login failed: " + string(result.body))
}
if c.client.Jar == nil {
c.client.Jar, err = cookiejar.New(nil)
if err != nil {
return err
}
}
u, err := url.Parse(c.config.Address)
if err != nil {
return err
}
c.client.Jar.SetCookies(u, result.cookies)
return nil
}
func (c *client) Logout(ctx context.Context) error {
ctx, span := trace.Start(ctx, "qbittorrent.Authentication.Logout")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/auth/logout", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
method: http.MethodPost,
url: apiUrl,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("logout failed: " + string(result.body))
}
return nil
}

View file

@ -0,0 +1,24 @@
package qbittorrent
import (
"context"
"testing"
)
func TestClient_Login(t *testing.T) {
ctx := context.Background()
if err := c.Authentication().Login(ctx); err != nil {
t.Fatal(err)
}
}
func TestClient_Logout(t *testing.T) {
ctx := context.Background()
if err := c.Authentication().Login(ctx); err != nil {
t.Fatal(err)
}
if err := c.Authentication().Logout(ctx); err != nil {
t.Fatal(err)
}
}

View file

@ -0,0 +1,72 @@
package qbittorrent
import (
"context"
"crypto/tls"
"net"
"net/http"
"time"
"go.opentelemetry.io/otel"
)
var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/server/pkg/qbittorrent")
// Client represents a qBittorrent client
type Client interface {
// Authentication auth qBittorrent client
Authentication() Authentication
// Application get qBittorrent application info
Application() Application
// Log get qBittorrent log
Log() Log
// Sync get qBittorrent events
Sync() Sync
// Transfer transfer manage
Transfer() Transfer
// Torrent manage for torrent
Torrent() Torrent
// Search api for search
Search() Search
// RSS api for rss
RSS() RSS
}
func NewClient(ctx context.Context, cfg *Config) (Client, error) {
var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
return c, nil
}
func LoginClient(ctx context.Context, cfg *Config) (Client, error) {
var c = &client{config: cfg, client: newClient(cfg.ConnectionMaxIdles, cfg.ConnectionTimeout)}
if err := c.Authentication().Login(ctx); err != nil {
return nil, err
}
if cfg.RefreshCookie {
go c.refreshCookie()
}
return c, nil
}
// newClient creates and returns a new clientPool
func newClient(maxIdle int, timeout time.Duration) *http.Client {
if maxIdle == 0 {
maxIdle = 128
}
if timeout == 0 {
timeout = time.Second * 3
}
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: maxIdle,
},
Timeout: timeout,
}
}

View file

@ -0,0 +1,128 @@
package qbittorrent
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type responseResult struct {
code int
body []byte
cookies []*http.Cookie
}
type requestData struct {
method string
url string
contentType string
body io.Reader
}
var _ Client = (*client)(nil)
type client struct {
config *Config
client *http.Client
}
func (c *client) Authentication() Authentication {
return c
}
func (c *client) Application() Application {
return c
}
func (c *client) Log() Log {
return c
}
func (c *client) Sync() Sync {
return c
}
func (c *client) Transfer() Transfer {
return c
}
func (c *client) Torrent() Torrent {
return c
}
func (c *client) Search() Search {
return c
}
func (c *client) RSS() RSS {
return c
}
// doRequest send request
func (c *client) doRequest(ctx context.Context, data *requestData) (*responseResult, error) {
if data.method == "" {
data.method = "GET"
}
if data.contentType == "" {
data.contentType = ContentTypeFormUrlEncoded
}
request, err := http.NewRequestWithContext(ctx, data.method, data.url, data.body)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", data.contentType)
for key, value := range c.config.CustomHeaders {
request.Header.Set(key, value)
}
resp, err := c.client.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
readAll, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return &responseResult{code: resp.StatusCode, body: readAll, cookies: resp.Cookies()}, nil
}
func (c *client) cookies() (string, error) {
if c.client.Jar == nil {
return "", ErrNotLogin
}
u, err := url.Parse(c.config.Address)
if err != nil {
return "", err
}
cookies := c.client.Jar.Cookies(u)
if len(cookies) == 0 {
return "", ErrNotLogin
}
var builder strings.Builder
for _, cookie := range cookies {
builder.WriteString(fmt.Sprintf("%s=%s; ", cookie.Name, cookie.Value))
}
return builder.String(), nil
}
func (c *client) refreshCookie() {
ctx := context.Background()
if c.config.RefreshIntervals == 0 {
c.config.RefreshIntervals = time.Hour
}
var ticker = time.NewTicker(c.config.RefreshIntervals)
for range ticker.C {
if err := c.Authentication().Logout(ctx); err != nil {
// todo
}
}
}

View file

@ -0,0 +1,56 @@
package qbittorrent
import (
"context"
"net/url"
"testing"
"time"
)
var (
c Client
)
func init() {
ctx := context.Background()
var err error
c, err = LoginClient(ctx, &Config{
Address: "http://192.168.3.33:38080",
Username: "admin",
Password: "J0710cz5",
RefreshIntervals: time.Hour,
ConnectionTimeout: time.Second * 3,
CustomHeaders: map[string]string{
//"Origin": "http://192.168.3.33:8080",
//"Referer": "http://192.168.3.33:8080",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
},
})
if err != nil {
panic(err)
}
}
func TestFormEncoder(t *testing.T) {
var option = LogOption{
Normal: true,
Info: true,
Warning: false,
Critical: false,
LastKnownId: 0,
}
var form = url.Values{}
err := encoder.Encode(option, form)
if err != nil {
t.Fatal(err)
}
t.Log(form)
}
func TestFormEncode(t *testing.T) {
var form = url.Values{}
form.Add("username", "admin hahaha")
form.Add("password", "J0710c?//&z5")
fe := form.Encode()
t.Log(fe)
}

View file

@ -0,0 +1,10 @@
package qbittorrent
import "github.com/gorilla/schema"
const (
ContentTypeJSON = "application/json"
ContentTypeFormUrlEncoded = "application/x-www-form-urlencoded"
)
var encoder = schema.NewEncoder()

View file

@ -0,0 +1,25 @@
package qbittorrent
import "time"
type Config struct {
// Address qBittorrent endpoint
Address string
// Username used to access the WebUI
Username string
// Password used to access the WebUI
Password string
// HTTP configuration
// CustomHeaders custom headers
CustomHeaders map[string]string
// ConnectionTimeout request timeout, default 3 seconds
ConnectionTimeout time.Duration
// ConnectionMaxIdles http client pool, default 128
ConnectionMaxIdles int
// RefreshCookie whether to automatically refresh cookies
RefreshCookie bool
// SessionTimeout interval for refreshing cookies, default 1 hour
RefreshIntervals time.Duration
}

View file

@ -0,0 +1,8 @@
package qbittorrent
import "errors"
var (
ErrNotLogin = errors.New("not login")
ErrAuthFailed = errors.New("auth failed")
)

View file

@ -0,0 +1,95 @@
package qbittorrent
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
)
type LogOption struct {
Normal bool `schema:"normal,omitempty"` // include normal messages
Info bool `schema:"info,omitempty"` // include info messages
Warning bool `schema:"warning,omitempty"` // include warning messages
Critical bool `schema:"critical,omitempty"` // include critical messages
LastKnownId int64 `schema:"last_known_id,omitempty"` // exclude messages with "message id" <= (default: last_known_id-1)
}
type LogEntry struct {
Id int `json:"id,omitempty"` // id of the message or peer
Timestamp int `json:"timestamp,omitempty"` // seconds since epoch
Type int `json:"type,omitempty"` // type of the message, Log::NORMAL: 1, Log::INFO: 2, Log::WARNING: 4, Log::CRITICAL: 8
Message string `json:"message,omitempty"` // text of the message
IP string `json:"ip"` // ip of the peer
Blocked bool `json:"blocked,omitempty"` // whether the peer was blocked
Reason string `json:"reason,omitempty"` // Reason of the block
}
type Log interface {
// GetLog get log
GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, error)
// GetPeerLog get peer log
GetPeerLog(ctx context.Context, lastKnownId int) ([]*LogEntry, error)
}
func (c *client) GetLog(ctx context.Context, option *LogOption) ([]*LogEntry, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Log.GetLog")
defer span.End()
var form = url.Values{}
err := encoder.Encode(option, form)
if err != nil {
return nil, err
}
apiUrl := fmt.Sprintf("%s/api/v2/log/main?%s", c.config.Address, form.Encode())
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
body: strings.NewReader(form.Encode()),
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get log failed: " + string(result.body))
}
var logs []*LogEntry
if err := json.Unmarshal(result.body, &logs); err != nil {
return nil, err
}
return logs, nil
}
func (c *client) GetPeerLog(ctx context.Context, lastKnownId int) ([]*LogEntry, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Log.GetPeerLog")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/log/peers", c.config.Address)
var form = url.Values{}
form.Add("last_known_id", strconv.Itoa(lastKnownId))
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
body: strings.NewReader(form.Encode()),
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get peer log failed: " + string(result.body))
}
var logs []*LogEntry
if err := json.Unmarshal(result.body, &logs); err != nil {
return nil, err
}
return logs, nil
}

View file

@ -0,0 +1,39 @@
package qbittorrent
import (
"context"
"encoding/json"
"testing"
)
func TestClient_GetLog(t *testing.T) {
ctx := context.Background()
entries, err := c.Log().GetLog(ctx, &LogOption{
Normal: true,
Info: true,
Warning: true,
Critical: true,
LastKnownId: 0,
})
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(entries)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetPeerLog(t *testing.T) {
ctx := context.Background()
entries, err := c.Log().GetPeerLog(ctx, -1)
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(entries)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}

View file

@ -0,0 +1,359 @@
package qbittorrent
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
type RSS interface {
// AddFolder create new folder for rss, full path of added folder such as "The Pirate Bay\Top100"
AddFolder(ctx context.Context, path string) error
// AddFeed add feed
AddFeed(ctx context.Context, option *RssAddFeedOption) error
// RemoveItem remove folder or feed
RemoveItem(ctx context.Context, path string) error
// MoveItem move or rename folder or feed
MoveItem(ctx context.Context, srcPath, destPath string) error
// GetItems list all items, if withData is true, will return all data
GetItems(ctx context.Context, withData bool) (map[string]interface{}, error)
// MarkAsRead if articleId is provided only the article is marked as read otherwise the whole feed
// is going to be marked as read.
MarkAsRead(ctx context.Context, option *RssMarkAsReadOption) error
// RefreshItem refresh folder or feed
RefreshItem(ctx context.Context, itemPath string) error
// SetAutoDownloadingRule set auto-downloading rule
SetAutoDownloadingRule(ctx context.Context, ruleName string, ruleDef *RssAutoDownloadingRuleDef) error
// RenameAutoDownloadingRule rename auto-downloading rule
RenameAutoDownloadingRule(ctx context.Context, ruleName, newRuleName string) error
// RemoveAutoDownloadingRule remove auto-downloading rule
RemoveAutoDownloadingRule(ctx context.Context, ruleName string) error
// GetAllAutoDownloadingRules get all auto-downloading rules
GetAllAutoDownloadingRules(ctx context.Context) (map[string]*RssAutoDownloadingRuleDef, error)
// GetAllArticlesMatchingRule get all articles matching a rule
GetAllArticlesMatchingRule(ctx context.Context, ruleName string) (map[string][]string, error)
}
type RssAddFeedOption struct {
// URL feed of rss such as http://thepiratebay.org/rss//top100/200
URL string `schema:"url"`
// Folder full path of added folder, optional
Folder string `schema:"path,omitempty"`
}
type RssMarkAsReadOption struct {
// ItemPath current full path of item
ItemPath string `schema:"itemPath"`
// ArticleId id of article, optional
ArticleId string `schema:"articleId,omitempty"`
}
type RssAutoDownloadingRuleDefTorrentParams struct {
Category string `json:"category,omitempty"`
DownloadLimit int `json:"download_limit,omitempty"`
DownloadPath int `json:"download_path,omitempty"`
InactiveSeedingTimeLimit int `json:"inactive_seeding_time_limit,omitempty"`
OperatingMode string `json:"operating_mode,omitempty"`
RatioLimit int `json:"ratio_limit,omitempty"`
SavePath string `json:"save_path,omitempty"`
SeedingTimeLimit int `json:"seeding_time_limit,omitempty"`
SkipChecking bool `json:"skip_checking,omitempty"`
Tags []string `json:"tags,omitempty"`
UploadLimit int `json:"upload_limit,omitempty"`
Stopped bool `json:"stopped,omitempty"`
UseAutoTMM bool `json:"use_auto_tmm,omitempty"`
}
type RssAutoDownloadingRuleDef struct {
AddPaused bool `json:"addPaused,omitempty"`
AffectedFeeds []string `json:"affectedFeeds,omitempty"`
AssignedCategory string `json:"assignedCategory,omitempty"`
Enabled bool `json:"enabled,omitempty"`
EpisodeFilter string `json:"episodeFilter,omitempty"`
IgnoreDays int `json:"ignoreDays,omitempty"`
LastMatch string `json:"lastMatch,omitempty"`
MustContain string `json:"mustContain,omitempty"`
MustNotContain string `json:"mustNotContain,omitempty"`
PreviouslyMatchedEpisodes []string `json:"previouslyMatchedEpisodes,omitempty"`
Priority int `json:"priority,omitempty"`
SavePath string `json:"savePath,omitempty"`
SmartFilter bool `json:"smartFilter,omitempty"`
TorrentParams *RssAutoDownloadingRuleDefTorrentParams `json:"torrentParams,omitempty"`
UseRegex bool `json:"useRegex,omitempty"`
}
func (c *client) AddFolder(ctx context.Context, path string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFolder")
defer span.End()
var formData = url.Values{}
formData.Add("path", path)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/addFolder", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("add rss folder failed: " + string(result.body))
}
return nil
}
func (c *client) AddFeed(ctx context.Context, opt *RssAddFeedOption) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.AddFeed")
defer span.End()
var formData = url.Values{}
err := encoder.Encode(opt, formData)
if err != nil {
return err
}
var apiUrl = fmt.Sprintf("%s/api/v2/rss/addFolder", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("add rss feed failed: " + string(result.body))
}
return nil
}
func (c *client) RemoveItem(ctx context.Context, path string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveItem")
defer span.End()
var formData = url.Values{}
formData.Add("path", path)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeItem", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("remove rss item failed: " + string(result.body))
}
return nil
}
func (c *client) MoveItem(ctx context.Context, srcPath, destPath string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.MoveItem")
defer span.End()
var formData = url.Values{}
formData.Add("itemPath", srcPath)
formData.Add("destPath", destPath)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/moveItem", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("move rss item failed: " + string(result.body))
}
return nil
}
func (c *client) GetItems(ctx context.Context, withData bool) (map[string]interface{}, error) {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetItems")
defer span.End()
var apiUrl = fmt.Sprintf("%s/api/v2/rss/items?withData=%t", c.config.Address, withData)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodGet,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get rss items failed: " + string(result.body))
}
var data = make(map[string]interface{})
err = json.Unmarshal(result.body, &data)
return data, err
}
func (c *client) MarkAsRead(ctx context.Context, opt *RssMarkAsReadOption) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.MarkAsRead")
defer span.End()
var formData = url.Values{}
err := encoder.Encode(opt, formData)
if err != nil {
return err
}
var apiUrl = fmt.Sprintf("%s/api/v2/rss/markAsRead", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("mark as read rss item failed: " + string(result.body))
}
return nil
}
func (c *client) RefreshItem(ctx context.Context, itemPath string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RefreshItem")
defer span.End()
var formData = url.Values{}
formData.Add("itemPath", itemPath)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/refreshItem", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("refresh rss item failed: " + string(result.body))
}
return nil
}
func (c *client) SetAutoDownloadingRule(ctx context.Context, ruleName string, ruleDef *RssAutoDownloadingRuleDef) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.SetAutoDownloadingRule")
defer span.End()
var formData = url.Values{}
formData.Add("ruleName", ruleName)
ruleDefBytes, err := json.Marshal(ruleDef)
if err != nil {
return err
}
formData.Add("ruleDef", string(ruleDefBytes))
var apiUrl = fmt.Sprintf("%s/api/v2/rss/setRule", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("set auto downloading rule failed: " + string(result.body))
}
return nil
}
func (c *client) RenameAutoDownloadingRule(ctx context.Context, ruleName, newRuleName string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RenameAutoDownloadingRule")
defer span.End()
var formData = url.Values{}
formData.Add("ruleName", ruleName)
formData.Add("newRuleName", newRuleName)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/renameRule", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("rename auto downloading rule failed: " + string(result.body))
}
return nil
}
func (c *client) RemoveAutoDownloadingRule(ctx context.Context, ruleName string) error {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.RemoveAutoDownloadingRule")
defer span.End()
var formData = url.Values{}
formData.Add("ruleName", ruleName)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/removeRule", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(formData.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("remove auto downloading rule failed: " + string(result.body))
}
return nil
}
func (c *client) GetAllAutoDownloadingRules(ctx context.Context) (map[string]*RssAutoDownloadingRuleDef, error) {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllAutoDownloadingRules")
defer span.End()
var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get rss rules failed: " + string(result.body))
}
var data = make(map[string]*RssAutoDownloadingRuleDef)
err = json.Unmarshal(result.body, &data)
return data, err
}
func (c *client) GetAllArticlesMatchingRule(ctx context.Context, ruleName string) (map[string][]string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.RSS.GetAllArticlesMatchingRule")
defer span.End()
var formData = url.Values{}
formData.Add("ruleName", ruleName)
var apiUrl = fmt.Sprintf("%s/api/v2/rss/matchingArticles?%s", c.config.Address, formData.Encode())
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get rss rule match articles failed: " + string(result.body))
}
var data = make(map[string][]string)
err = json.Unmarshal(result.body, &data)
return data, err
}

View file

@ -0,0 +1,64 @@
package qbittorrent
type Search interface {
Start()
Stop()
Status()
Results()
Delete()
Plugins()
InstallPlugins()
UninstallPlugins()
EnableSearchPlugins()
UpdateSearchPlugins()
}
func (c *client) Start() {
//TODO implement me
panic("implement me")
}
func (c *client) Stop() {
//TODO implement me
panic("implement me")
}
func (c *client) Status() {
//TODO implement me
panic("implement me")
}
func (c *client) Results() {
//TODO implement me
panic("implement me")
}
func (c *client) Delete() {
//TODO implement me
panic("implement me")
}
func (c *client) Plugins() {
//TODO implement me
panic("implement me")
}
func (c *client) InstallPlugins() {
//TODO implement me
panic("implement me")
}
func (c *client) UninstallPlugins() {
//TODO implement me
panic("implement me")
}
func (c *client) EnableSearchPlugins() {
//TODO implement me
panic("implement me")
}
func (c *client) UpdateSearchPlugins() {
//TODO implement me
panic("implement me")
}

View file

@ -0,0 +1,127 @@
package qbittorrent
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
)
type Sync interface {
// MainData get sync main data, rid is Response ID. if not provided, will be assumed.
// if the given is different from the one of last server reply, will be (see the server reply details for more info)
MainData(ctx context.Context, rid int) (*SyncMainData, error)
// TorrentPeersData get sync torrent peer data, hash is torrent hash, rid is response id
TorrentPeersData(ctx context.Context, hash string, rid int) (*SyncTorrentPeers, error)
}
type SyncMainData struct {
Rid int `json:"rid,omitempty"`
FullUpdate bool `json:"full_update,omitempty"`
ServerState ServerState `json:"server_state,omitempty"`
Torrents map[string]SyncTorrentInfo `json:"torrents,omitempty"`
}
type ServerState struct {
AllTimeDl int64 `json:"alltime_dl,omitempty"`
AllTimeUl int64 `json:"alltime_ul,omitempty"`
AverageTimeQueue int `json:"average_time_queue,omitempty"`
DlInfoData int64 `json:"dl_info_data,omitempty"`
DlInfoSpeed int `json:"dl_info_speed,omitempty"`
QueuedIoJobs int `json:"queued_io_jobs,omitempty"`
TotalBuffersSize int `json:"total_buffers_size,omitempty"`
UpInfoData int64 `json:"up_info_data,omitempty"`
UpInfoSpeed int `json:"up_info_speed,omitempty"`
WriteCacheOverload string `json:"write_cache_overload,omitempty"`
}
type SyncTorrentInfo struct {
AmountLeft int64 `json:"amount_left,omitempty"`
Completed int `json:"completed,omitempty"`
DlSpeed int `json:"dlspeed,omitempty"`
Downloaded int `json:"downloaded,omitempty"`
DownloadedSession int `json:"downloaded_session,omitempty"`
Eta int `json:"eta,omitempty"`
Progress float64 `json:"progress,omitempty"`
SeenComplete int `json:"seen_complete,omitempty"`
TimeActive int `json:"time_active,omitempty"`
}
type SyncTorrentPeers struct {
Rid int `json:"rid,omitempty"`
FullUpdate bool `json:"full_update,omitempty"`
ShowFlags bool `json:"show_flags,omitempty"`
Peers map[string]SyncTorrentPeer `json:"peers,omitempty"`
}
type SyncTorrentPeer struct {
Client string `json:"client,omitempty"`
Connection string `json:"connection,omitempty"`
Country string `json:"country,omitempty"`
CountryCode string `json:"country_code,omitempty"`
DlSpeed int `json:"dl_speed,omitempty"`
Downloaded int `json:"downloaded,omitempty"`
Files string `json:"files,omitempty"`
Flags string `json:"flags,omitempty"`
FlagsDesc string `json:"flags_desc,omitempty"`
IP string `json:"ip,omitempty"`
PeerIDClient string `json:"peer_id_client,omitempty"`
Port int `json:"port,omitempty"`
Progress float64 `json:"progress,omitempty"`
Relevance float64 `json:"relevance,omitempty"`
UpSpeed int `json:"up_speed,omitempty"`
Uploaded int `json:"uploaded,omitempty"`
}
func (c *client) MainData(ctx context.Context, rid int) (*SyncMainData, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Sync.MainData")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/sync/maindata?rid=%d", c.config.Address, rid)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get main data failed: " + string(result.body))
}
var mainData = new(SyncMainData)
if err := json.Unmarshal(result.body, mainData); err != nil {
return nil, err
}
return mainData, nil
}
func (c *client) TorrentPeersData(ctx context.Context, hash string, rid int) (*SyncTorrentPeers, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Sync.TorrentPeersData")
defer span.End()
var formData = url.Values{}
formData.Add("hash", hash)
formData.Add("rid", strconv.Itoa(rid))
apiUrl := fmt.Sprintf("%s/api/v2/sync/torrentPeers?%s", c.config.Address, formData.Encode())
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get torrent peers data failed: " + string(result.body))
}
var mainData = new(SyncTorrentPeers)
if err := json.Unmarshal(result.body, mainData); err != nil {
return nil, err
}
return mainData, nil
}

View file

@ -0,0 +1,37 @@
package qbittorrent
import (
"context"
"encoding/json"
"testing"
"time"
)
func TestClient_MainData(t *testing.T) {
ctx := context.Background()
syncMainData, err := c.Sync().MainData(ctx, 0)
if err != nil {
t.Fatal(err)
}
t.Logf("sync main data: %+v", syncMainData)
time.Sleep(time.Second)
syncMainData, err = c.Sync().MainData(ctx, 0)
if err != nil {
t.Fatal(err)
}
t.Logf("sync main data: %+v", syncMainData)
}
func TestClient_TorrentPeersData(t *testing.T) {
ctx := context.Background()
peersData, err := c.Sync().TorrentPeersData(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc", 0)
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(peersData)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,299 @@
package qbittorrent
import (
"context"
"encoding/json"
"os"
"testing"
)
func TestClient_GetTorrents(t *testing.T) {
ctx := context.Background()
torrents, err := c.Torrent().GetTorrents(ctx, &TorrentOption{
Filter: "",
Category: "movies",
Tag: "hdtime",
Sort: "",
Reverse: false,
Limit: 0,
Offset: 0,
Hashes: nil,
})
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(torrents)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetProperties(t *testing.T) {
ctx := context.Background()
properties, err := c.Torrent().GetProperties(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(properties)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetTrackers(t *testing.T) {
ctx := context.Background()
trackers, err := c.Torrent().GetTrackers(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(trackers)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetWebSeeds(t *testing.T) {
ctx := context.Background()
webSeeds, err := c.Torrent().GetWebSeeds(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(webSeeds)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetContents(t *testing.T) {
ctx := context.Background()
contents, err := c.Torrent().GetContents(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
bytes, err := json.Marshal(contents)
if err != nil {
t.Fatal(err)
}
t.Log(string(bytes))
}
func TestClient_GetPiecesStates(t *testing.T) {
ctx := context.Background()
states, err := c.Torrent().GetPiecesStates(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
t.Log(states)
}
func TestClient_GetPiecesHashes(t *testing.T) {
ctx := context.Background()
hashes, err := c.Torrent().GetPiecesHashes(ctx, "f23daefbe8d24d3dd882b44cb0b4f762bc23b4fc")
if err != nil {
t.Fatal(err)
}
t.Log(hashes)
}
func TestClient_PauseTorrents(t *testing.T) {
ctx := context.Background()
err := c.Torrent().PauseTorrents(ctx, []string{"202382999be6a4fab395cd9c2c9d294177587904"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent paused")
}
func TestClient_ResumeTorrents(t *testing.T) {
ctx := context.Background()
err := c.Torrent().ResumeTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent resumed")
}
func TestClient_DeleteTorrents(t *testing.T) {
ctx := context.Background()
err := c.Torrent().DeleteTorrents(ctx, []string{"202382999be6a4fab395cd9c2c9d294177587904"}, true)
if err != nil {
t.Fatal(err)
}
t.Log("torrent deleted")
}
func TestClient_RecheckTorrents(t *testing.T) {
ctx := context.Background()
err := c.Torrent().RecheckTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent rechecked")
}
func TestClient_ReAnnounceTorrents(t *testing.T) {
ctx := context.Background()
err := c.Torrent().ReAnnounceTorrents(ctx, []string{"fd3b4bf1937c59a8fd1a240cddc07172e0b979a2"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent reannounceed")
}
func TestClient_AddNewTorrent(t *testing.T) {
ctx := context.Background()
fileContent, err := os.ReadFile("C:\\Users\\xuthu\\Downloads\\bbbbb.torrent")
if err != nil {
t.Fatal(err)
}
err = c.Torrent().AddNewTorrent(ctx, &TorrentAddOption{
Torrents: []*TorrentAddFileMetadata{
{
//Filename: "ttttt.torrent",
Data: fileContent,
},
},
Category: "movies",
Tags: []string{"d", "e", "f"},
SkipChecking: false,
Paused: false,
RootFolder: false,
Rename: "",
UpLimit: 0,
DlLimit: 0,
RatioLimit: 0,
SeedingTimeLimit: 0,
AutoTMM: false,
SequentialDownload: "",
FirstLastPiecePrio: "",
})
if err != nil {
t.Fatal(err)
}
t.Log("torrent added")
}
func TestClient_AddTrackers(t *testing.T) {
ctx := context.Background()
err := c.Torrent().AddTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", []string{"https://hddtime.org/announce"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent trackers added")
}
func TestClient_EditTrackers(t *testing.T) {
ctx := context.Background()
err := c.Torrent().EditTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", "https://hddtime.org/announce", "https://hdctime.org/announce")
if err != nil {
t.Fatal(err)
}
t.Log("torrent trackers edited")
}
func TestClient_RemoveTrackers(t *testing.T) {
ctx := context.Background()
err := c.Torrent().RemoveTrackers(ctx, "ca4523a3db9c6c3a13d7d7f3a545f97b75083032", []string{"https://hdctime.org/announce"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent trackers removed")
}
func TestClient_AddPeers(t *testing.T) {
// todo no test
//c.Torrent().AddPeers([]string{"ca4523a3db9c6c3a13d7d7f3a545f97b75083032"}, []string{"10.0.0.1:38080"})
}
func TestClient_IncreasePriority(t *testing.T) {
ctx := context.Background()
err := c.Torrent().IncreasePriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent priority increased")
}
func TestClient_DecreasePriority(t *testing.T) {
ctx := context.Background()
err := c.Torrent().DecreasePriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent priority decreased")
}
func TestClient_MaxPriority(t *testing.T) {
ctx := context.Background()
err := c.Torrent().MaxPriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent priority maxed")
}
func TestClient_MinPriority(t *testing.T) {
ctx := context.Background()
err := c.Torrent().MinPriority(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent priority mined")
}
func TestClient_SetFilePriority(t *testing.T) {
// todo no test
}
func TestClient_GetDownloadLimit(t *testing.T) {
ctx := context.Background()
downloadLimit, err := c.Torrent().GetDownloadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent download limit", downloadLimit)
}
func TestClient_SetDownloadLimit(t *testing.T) {
ctx := context.Background()
err := c.Torrent().SetDownloadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, 0)
if err != nil {
t.Fatal(err)
}
t.Log("torrent download limit setted")
}
func TestClient_SetShareLimit(t *testing.T) {
ctx := context.Background()
err := c.Torrent().SetShareLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, -2, -2, -2)
if err != nil {
t.Fatal(err)
}
t.Log("torrent share limit setted")
}
func TestClient_GetUploadLimit(t *testing.T) {
ctx := context.Background()
limit, err := c.Torrent().GetUploadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"})
if err != nil {
t.Fatal(err)
}
t.Log("torrent upload limit", limit)
}
func TestClient_SetUploadLimit(t *testing.T) {
ctx := context.Background()
err := c.Torrent().SetUploadLimit(ctx, []string{"916a250d32822adca39eb2b53efadfda1a15f902"}, 0)
if err != nil {
t.Fatal(err)
}
t.Log("torrent upload limit setted")
}
func TestClient_SetLocation(t *testing.T) {
// todo test
}

View file

@ -0,0 +1,209 @@
package qbittorrent
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
type TransferStatusBar struct {
ConnectionStatus string `json:"connection_status,omitempty"`
DhtNodes int `json:"dht_nodes,omitempty"`
DlInfoData int64 `json:"dl_info_data,omitempty"`
DlInfoSpeed int `json:"dl_info_speed,omitempty"`
DlRateLimit int `json:"dl_rate_limit,omitempty"`
UpInfoData int `json:"up_info_data,omitempty"`
UpInfoSpeed int `json:"up_info_speed,omitempty"`
UpRateLimit int `json:"up_rate_limit,omitempty"`
Queueing bool `json:"queueing,omitempty"`
UseAltSpeedLimits bool `json:"use_alt_speed_limits,omitempty"`
RefreshInterval int `json:"refresh_interval,omitempty"`
}
type Transfer interface {
// GlobalStatusBar usually see in qBittorrent status bar
GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error)
// BanPeers the peer to ban, or multiple peers separated by a pipe.
// each peer is host:port
BanPeers(ctx context.Context, peers []string) error
// GetSpeedLimitsMode get alternative speed limits state
GetSpeedLimitsMode(ctx context.Context) (string, error)
// ToggleSpeedLimitsMode toggle alternative speed limits
ToggleSpeedLimitsMode(ctx context.Context) error
// GetGlobalUploadLimit get global upload limit, the response is the value of current global download speed
// limit in bytes/second; this value will be zero if no limit is applied.
GetGlobalUploadLimit(ctx context.Context) (string, error)
// SetGlobalUploadLimit set global upload limit, set in bytes/second
SetGlobalUploadLimit(ctx context.Context, limit int) error
// GetGlobalDownloadLimit get global download limit, the response is the value of current global download speed
// limit in bytes/second; this value will be zero if no limit is applied.
GetGlobalDownloadLimit(ctx context.Context) (string, error)
// SetGlobalDownloadLimit set global download limit, set in bytes/second
SetGlobalDownloadLimit(ctx context.Context, limit int) error
}
func (c *client) GlobalStatusBar(ctx context.Context) (*TransferStatusBar, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GlobalStatusBar")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/info", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return nil, err
}
if result.code != 200 {
return nil, errors.New("get global transfer status bar failed: " + string(result.body))
}
var data = new(TransferStatusBar)
if err := json.Unmarshal(result.body, data); err != nil {
return nil, err
}
return data, nil
}
func (c *client) BanPeers(ctx context.Context, peers []string) error {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.BanPeers")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/banPeers", c.config.Address)
var form = url.Values{}
form.Add("peers", strings.Join(peers, "|"))
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
body: strings.NewReader(form.Encode()),
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("ban peers failed: " + string(result.body))
}
return nil
}
func (c *client) GetSpeedLimitsMode(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetSpeedLimitsMode")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/speedLimitsMode", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("ban peers failed: " + string(result.body))
}
return string(result.body), nil
}
func (c *client) ToggleSpeedLimitsMode(ctx context.Context) error {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.ToggleSpeedLimitsMode")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/toggleSpeedLimitsMode", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
method: http.MethodPost,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("ban peers failed: " + string(result.body))
}
return nil
}
func (c *client) GetGlobalUploadLimit(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalUploadLimit")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/uploadLimit", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("get global upload limit failed: " + string(result.body))
}
return string(result.body), nil
}
func (c *client) SetGlobalUploadLimit(ctx context.Context, limit int) error {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalUploadLimit")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/setUploadLimit?limit=%d", c.config.Address, limit)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("set global upload limit failed: " + string(result.body))
}
return nil
}
func (c *client) GetGlobalDownloadLimit(ctx context.Context) (string, error) {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.GetGlobalDownloadLimit")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/downloadLimit", c.config.Address)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return "", err
}
if result.code != 200 {
return "", errors.New("get global download limit failed: " + string(result.body))
}
return string(result.body), nil
}
func (c *client) SetGlobalDownloadLimit(ctx context.Context, limit int) error {
ctx, span := trace.Start(ctx, "qbittorrent.Transfer.SetGlobalDownloadLimit")
defer span.End()
apiUrl := fmt.Sprintf("%s/api/v2/transfer/setDownloadLimit?limit=%d", c.config.Address, limit)
result, err := c.doRequest(ctx, &requestData{
url: apiUrl,
})
if err != nil {
return err
}
if result.code != 200 {
return errors.New("set global download limit failed: " + string(result.body))
}
return nil
}