mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
fix(qbittorrent): params url parsing (#286)
* fix(qbittorrent): params url parsing * feat: add more logging * refactor: qbit tracker status check
This commit is contained in:
parent
52fad1da95
commit
a5ade5ef24
4 changed files with 144 additions and 60 deletions
|
@ -3,6 +3,7 @@ package action
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
@ -76,8 +77,7 @@ func (s *service) qbittorrent(qbt *qbittorrent.Client, action domain.Action, rel
|
||||||
}
|
}
|
||||||
|
|
||||||
if !action.Paused && !action.ReAnnounceSkip && release.TorrentHash != "" {
|
if !action.Paused && !action.ReAnnounceSkip && release.TorrentHash != "" {
|
||||||
err = s.checkTrackerStatus(qbt, action, release.TorrentHash)
|
if err := s.reannounceTorrent(qbt, action, release.TorrentHash); err != nil {
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Stack().Err(err).Msgf("could not reannounce torrent: %v", release.TorrentHash)
|
s.log.Error().Stack().Err(err).Msgf("could not reannounce torrent: %v", release.TorrentHash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -167,17 +167,13 @@ func (s *service) qbittorrentCheckRulesCanDownload(action domain.Action) (bool,
|
||||||
return true, qbt, nil
|
return true, qbt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) checkTrackerStatus(qb *qbittorrent.Client, action domain.Action, hash string) error {
|
func (s *service) reannounceTorrent(qb *qbittorrent.Client, action domain.Action, hash string) error {
|
||||||
announceOK := false
|
announceOK := false
|
||||||
attempts := 0
|
attempts := 0
|
||||||
|
|
||||||
// initial sleep to give tracker a head start
|
|
||||||
interval := ReannounceInterval
|
interval := ReannounceInterval
|
||||||
if action.ReAnnounceInterval == 0 {
|
if action.ReAnnounceInterval > 0 {
|
||||||
time.Sleep(6 * time.Second)
|
|
||||||
} else {
|
|
||||||
interval = int(action.ReAnnounceInterval)
|
interval = int(action.ReAnnounceInterval)
|
||||||
time.Sleep(time.Duration(interval) * time.Second)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
maxAttempts := ReannounceMaxAttempts
|
maxAttempts := ReannounceMaxAttempts
|
||||||
|
@ -188,16 +184,24 @@ func (s *service) checkTrackerStatus(qb *qbittorrent.Client, action domain.Actio
|
||||||
for attempts < maxAttempts {
|
for attempts < maxAttempts {
|
||||||
s.log.Debug().Msgf("qBittorrent - run re-announce %v attempt: %v", hash, attempts)
|
s.log.Debug().Msgf("qBittorrent - run re-announce %v attempt: %v", hash, attempts)
|
||||||
|
|
||||||
|
// add delay for next run
|
||||||
|
time.Sleep(time.Duration(interval) * time.Second)
|
||||||
|
|
||||||
trackers, err := qb.GetTorrentTrackers(hash)
|
trackers, err := qb.GetTorrentTrackers(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error().Err(err).Msgf("qBittorrent - could not get trackers for torrent: %v", hash)
|
s.log.Error().Err(err).Msgf("qBittorrent - could not get trackers for torrent: %v", hash)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if trackers == nil {
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
s.log.Trace().Msgf("qBittorrent - run re-announce %v attempt: %v trackers (%+v)", hash, attempts, trackers)
|
s.log.Trace().Msgf("qBittorrent - run re-announce %v attempt: %v trackers (%+v)", hash, attempts, trackers)
|
||||||
|
|
||||||
// check if status not working or something else
|
// check if status not working or something else
|
||||||
working := findTrackerStatus(trackers)
|
working := isTrackerStatusOK(trackers)
|
||||||
if working {
|
if working {
|
||||||
s.log.Debug().Msgf("qBittorrent - re-announce for %v OK", hash)
|
s.log.Debug().Msgf("qBittorrent - re-announce for %v OK", hash)
|
||||||
|
|
||||||
|
@ -214,15 +218,9 @@ func (s *service) checkTrackerStatus(qb *qbittorrent.Client, action domain.Actio
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add delay for next run
|
|
||||||
time.Sleep(time.Duration(interval) * time.Second)
|
|
||||||
|
|
||||||
attempts++
|
attempts++
|
||||||
}
|
}
|
||||||
|
|
||||||
// add extra delay before delete
|
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
|
|
||||||
// delete on failure to reannounce
|
// delete on failure to reannounce
|
||||||
if !announceOK && action.ReAnnounceDelete {
|
if !announceOK && action.ReAnnounceDelete {
|
||||||
s.log.Debug().Msgf("qBittorrent - re-announce for %v took too long, deleting torrent", hash)
|
s.log.Debug().Msgf("qBittorrent - re-announce for %v took too long, deleting torrent", hash)
|
||||||
|
@ -244,13 +242,32 @@ func (s *service) checkTrackerStatus(qb *qbittorrent.Client, action domain.Actio
|
||||||
// 2 Tracker has been contacted and is working
|
// 2 Tracker has been contacted and is working
|
||||||
// 3 Tracker is updating
|
// 3 Tracker is updating
|
||||||
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||||
func findTrackerStatus(slice []qbittorrent.TorrentTracker) bool {
|
func isTrackerStatusOK(trackers []qbittorrent.TorrentTracker) bool {
|
||||||
for _, item := range slice {
|
for _, tracker := range trackers {
|
||||||
if item.Status == qbittorrent.TrackerStatusDisabled {
|
if tracker.Status == qbittorrent.TrackerStatusDisabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Status == qbittorrent.TrackerStatusOK {
|
// check for certain messages before the tracker status to catch ok status with unreg msg
|
||||||
|
if isUnregistered(tracker.Message) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tracker.Status == qbittorrent.TrackerStatusOK {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnregistered(msg string) bool {
|
||||||
|
words := []string{"unregistered", "not registered", "not found", "not exist"}
|
||||||
|
|
||||||
|
msg = strings.ToLower(msg)
|
||||||
|
|
||||||
|
for _, v := range words {
|
||||||
|
if strings.Contains(msg, v) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, e
|
||||||
var err error
|
var err error
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
|
|
||||||
reqUrl := buildUrl(c.settings, endpoint)
|
reqUrl := buildUrlOpts(c.settings, endpoint, opts)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", reqUrl, nil)
|
req, err := http.NewRequest("GET", reqUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -296,7 +296,8 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) setCookies(cookies []*http.Cookie) {
|
func (c *Client) setCookies(cookies []*http.Cookie) {
|
||||||
cookieURL, _ := url.Parse(fmt.Sprintf("%v://%v:%v", c.settings.protocol, c.settings.Hostname, c.settings.Port))
|
cookieURL, _ := url.Parse(buildUrl(c.settings, ""))
|
||||||
|
|
||||||
c.http.Jar.SetCookies(cookieURL, cookies)
|
c.http.Jar.SetCookies(cookieURL, cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,3 +347,58 @@ func buildUrl(settings Settings, endpoint string) string {
|
||||||
// make into new string and return
|
// make into new string and return
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildUrlOpts(settings Settings, endpoint string, opts map[string]string) string {
|
||||||
|
// parse url
|
||||||
|
u, _ := url.Parse(settings.Hostname)
|
||||||
|
|
||||||
|
// reset Opaque
|
||||||
|
u.Opaque = ""
|
||||||
|
|
||||||
|
// set scheme
|
||||||
|
scheme := "http"
|
||||||
|
if u.Scheme == "http" || u.Scheme == "https" {
|
||||||
|
if settings.TLS {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
u.Scheme = scheme
|
||||||
|
} else {
|
||||||
|
if settings.TLS {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
u.Scheme = scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// if host is empty lets use one from settings
|
||||||
|
if u.Host == "" {
|
||||||
|
u.Host = settings.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset Path
|
||||||
|
if u.Host == u.Path {
|
||||||
|
u.Path = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle ports
|
||||||
|
if settings.Port > 0 {
|
||||||
|
if settings.Port == 80 || settings.Port == 443 {
|
||||||
|
// skip for regular http and https
|
||||||
|
} else {
|
||||||
|
u.Host = fmt.Sprintf("%v:%v", u.Host, settings.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add query params
|
||||||
|
q := u.Query()
|
||||||
|
for k, v := range opts {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
// join path
|
||||||
|
u.Path = path.Join(u.Path, "/api/v2/", endpoint)
|
||||||
|
|
||||||
|
// make into new string and return
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
|
@ -50,12 +50,12 @@ type TorrentTrackersResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorrentTracker struct {
|
type TorrentTracker struct {
|
||||||
//Tier uint `json:"tier"` // can be both empty "" and int
|
//Tier int `json:"tier"` // can be both empty "" and int
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Status TrackerStatus `json:"status"`
|
Status TrackerStatus `json:"status"`
|
||||||
NumPeers int `json:"num_peers"`
|
NumPeers int `json:"num_peers"`
|
||||||
NumSeeds int `json:"num_seeds"`
|
NumSeeds int `json:"num_seeds"`
|
||||||
NumLeechers int `json:"num_leechers"`
|
NumLeechers int `json:"num_leeches"`
|
||||||
NumDownloaded int `json:"num_downloaded"`
|
NumDownloaded int `json:"num_downloaded"`
|
||||||
Message string `json:"msg"`
|
Message string `json:"msg"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/http/httputil"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -14,11 +14,12 @@ import (
|
||||||
|
|
||||||
// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication
|
// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication
|
||||||
func (c *Client) Login() error {
|
func (c *Client) Login() error {
|
||||||
credentials := make(map[string]string)
|
opts := map[string]string{
|
||||||
credentials["username"] = c.settings.Username
|
"username": c.settings.Username,
|
||||||
credentials["password"] = c.settings.Password
|
"password": c.settings.Password,
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := c.postBasic("auth/login", credentials)
|
resp, err := c.postBasic("auth/login", opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("login error")
|
log.Error().Err(err).Msg("login error")
|
||||||
return err
|
return err
|
||||||
|
@ -57,7 +58,6 @@ func (c *Client) Login() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTorrents() ([]Torrent, error) {
|
func (c *Client) GetTorrents() ([]Torrent, error) {
|
||||||
var torrents []Torrent
|
|
||||||
|
|
||||||
resp, err := c.get("torrents/info", nil)
|
resp, err := c.get("torrents/info", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -73,6 +73,7 @@ func (c *Client) GetTorrents() ([]Torrent, error) {
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var torrents []Torrent
|
||||||
err = json.Unmarshal(body, &torrents)
|
err = json.Unmarshal(body, &torrents)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("get torrents unmarshal error")
|
log.Error().Err(err).Msg("get torrents unmarshal error")
|
||||||
|
@ -83,13 +84,11 @@ func (c *Client) GetTorrents() ([]Torrent, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
||||||
var torrents []Torrent
|
opts := map[string]string{
|
||||||
|
"filter": string(filter),
|
||||||
|
}
|
||||||
|
|
||||||
v := url.Values{}
|
resp, err := c.get("torrents/info", opts)
|
||||||
v.Add("filter", string(filter))
|
|
||||||
params := v.Encode()
|
|
||||||
|
|
||||||
resp, err := c.get("torrents/info?"+params, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
|
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -103,6 +102,7 @@ func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var torrents []Torrent
|
||||||
err = json.Unmarshal(body, &torrents)
|
err = json.Unmarshal(body, &torrents)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter)
|
log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter)
|
||||||
|
@ -115,11 +115,11 @@ func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
||||||
func (c *Client) GetTorrentsActiveDownloads() ([]Torrent, error) {
|
func (c *Client) GetTorrentsActiveDownloads() ([]Torrent, error) {
|
||||||
var filter = TorrentFilterDownloading
|
var filter = TorrentFilterDownloading
|
||||||
|
|
||||||
v := url.Values{}
|
opts := map[string]string{
|
||||||
v.Add("filter", string(filter))
|
"filter": string(filter),
|
||||||
params := v.Encode()
|
}
|
||||||
|
|
||||||
resp, err := c.get("torrents/info?"+params, nil)
|
resp, err := c.get("torrents/info", opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
|
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -167,14 +167,11 @@ func (c *Client) GetTorrentsRaw() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
|
func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
|
||||||
var trackers []TorrentTracker
|
opts := map[string]string{
|
||||||
|
"hash": hash,
|
||||||
|
}
|
||||||
|
|
||||||
params := url.Values{}
|
resp, err := c.get("torrents/trackers", opts)
|
||||||
params.Add("hash", hash)
|
|
||||||
|
|
||||||
p := params.Encode()
|
|
||||||
|
|
||||||
resp, err := c.get("torrents/trackers?"+p, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("get torrent trackers error: %v", hash)
|
log.Error().Err(err).Msgf("get torrent trackers error: %v", hash)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -182,12 +179,30 @@ func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
dump, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("get torrent trackers dump response error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("get torrent trackers response dump: %v", string(dump))
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
//return nil, fmt.Errorf("torrent not found: %v", hash)
|
||||||
|
return nil, nil
|
||||||
|
} else if resp.StatusCode == http.StatusForbidden {
|
||||||
|
//return nil, fmt.Errorf("torrent not found: %v", hash)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
body, readErr := ioutil.ReadAll(resp.Body)
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash)
|
log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash)
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("get torrent trackers body: %v", string(body))
|
||||||
|
|
||||||
|
var trackers []TorrentTracker
|
||||||
err = json.Unmarshal(body, &trackers)
|
err = json.Unmarshal(body, &trackers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("get torrent trackers: %v", hash)
|
log.Error().Err(err).Msgf("get torrent trackers: %v", hash)
|
||||||
|
@ -215,16 +230,15 @@ func (c *Client) AddTorrentFromFile(file string, options map[string]string) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
||||||
v := url.Values{}
|
|
||||||
|
|
||||||
// Add hashes together with | separator
|
// Add hashes together with | separator
|
||||||
hv := strings.Join(hashes, "|")
|
hv := strings.Join(hashes, "|")
|
||||||
v.Add("hashes", hv)
|
|
||||||
v.Add("deleteFiles", strconv.FormatBool(deleteFiles))
|
|
||||||
|
|
||||||
encodedHashes := v.Encode()
|
opts := map[string]string{
|
||||||
|
"hashes": hv,
|
||||||
|
"deleteFiles": strconv.FormatBool(deleteFiles),
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := c.get("torrents/delete?"+encodedHashes, nil)
|
resp, err := c.get("torrents/delete", opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("delete torrents error: %v", hashes)
|
log.Error().Err(err).Msgf("delete torrents error: %v", hashes)
|
||||||
return err
|
return err
|
||||||
|
@ -239,15 +253,13 @@ func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
||||||
v := url.Values{}
|
|
||||||
|
|
||||||
// Add hashes together with | separator
|
// Add hashes together with | separator
|
||||||
hv := strings.Join(hashes, "|")
|
hv := strings.Join(hashes, "|")
|
||||||
v.Add("hashes", hv)
|
opts := map[string]string{
|
||||||
|
"hashes": hv,
|
||||||
|
}
|
||||||
|
|
||||||
encodedHashes := v.Encode()
|
resp, err := c.get("torrents/reannounce", opts)
|
||||||
|
|
||||||
resp, err := c.get("torrents/reannounce?"+encodedHashes, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("re-announce error: %v", hashes)
|
log.Error().Err(err).Msgf("re-announce error: %v", hashes)
|
||||||
return err
|
return err
|
||||||
|
@ -262,8 +274,6 @@ func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetTransferInfo() (*TransferInfo, error) {
|
func (c *Client) GetTransferInfo() (*TransferInfo, error) {
|
||||||
var info TransferInfo
|
|
||||||
|
|
||||||
resp, err := c.get("transfer/info", nil)
|
resp, err := c.get("transfer/info", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("get torrents error")
|
log.Error().Err(err).Msg("get torrents error")
|
||||||
|
@ -278,6 +288,7 @@ func (c *Client) GetTransferInfo() (*TransferInfo, error) {
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var info TransferInfo
|
||||||
err = json.Unmarshal(body, &info)
|
err = json.Unmarshal(body, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("get torrents unmarshal error")
|
log.Error().Err(err).Msg("get torrents unmarshal error")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue