diff --git a/internal/action/qbittorrent.go b/internal/action/qbittorrent.go index 36edbb6..b6e148f 100644 --- a/internal/action/qbittorrent.go +++ b/internal/action/qbittorrent.go @@ -3,6 +3,7 @@ package action import ( "context" "strconv" + "strings" "time" "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 != "" { - err = s.checkTrackerStatus(qbt, action, release.TorrentHash) - if err != nil { + if err := s.reannounceTorrent(qbt, action, release.TorrentHash); err != nil { s.log.Error().Stack().Err(err).Msgf("could not reannounce torrent: %v", release.TorrentHash) return err } @@ -167,17 +167,13 @@ func (s *service) qbittorrentCheckRulesCanDownload(action domain.Action) (bool, 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 attempts := 0 - // initial sleep to give tracker a head start interval := ReannounceInterval - if action.ReAnnounceInterval == 0 { - time.Sleep(6 * time.Second) - } else { + if action.ReAnnounceInterval > 0 { interval = int(action.ReAnnounceInterval) - time.Sleep(time.Duration(interval) * time.Second) } maxAttempts := ReannounceMaxAttempts @@ -188,16 +184,24 @@ func (s *service) checkTrackerStatus(qb *qbittorrent.Client, action domain.Actio for attempts < maxAttempts { 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) if err != nil { s.log.Error().Err(err).Msgf("qBittorrent - could not get trackers for torrent: %v", hash) return err } + if trackers == nil { + attempts++ + continue + } + s.log.Trace().Msgf("qBittorrent - run re-announce %v attempt: %v trackers (%+v)", hash, attempts, trackers) // check if status not working or something else - working := findTrackerStatus(trackers) + working := isTrackerStatusOK(trackers) if working { 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 } - // add delay for next run - time.Sleep(time.Duration(interval) * time.Second) - attempts++ } - // add extra delay before delete - time.Sleep(30 * time.Second) - // delete on failure to reannounce if !announceOK && action.ReAnnounceDelete { 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 // 3 Tracker is updating // 4 Tracker has been contacted, but it is not working (or doesn't send proper replies) -func findTrackerStatus(slice []qbittorrent.TorrentTracker) bool { - for _, item := range slice { - if item.Status == qbittorrent.TrackerStatusDisabled { +func isTrackerStatusOK(trackers []qbittorrent.TorrentTracker) bool { + for _, tracker := range trackers { + if tracker.Status == qbittorrent.TrackerStatusDisabled { 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 } } diff --git a/pkg/qbittorrent/client.go b/pkg/qbittorrent/client.go index 8a1b008..220a137 100644 --- a/pkg/qbittorrent/client.go +++ b/pkg/qbittorrent/client.go @@ -88,7 +88,7 @@ func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, e var err error var resp *http.Response - reqUrl := buildUrl(c.settings, endpoint) + reqUrl := buildUrlOpts(c.settings, endpoint, opts) req, err := http.NewRequest("GET", reqUrl, 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) { - 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) } @@ -346,3 +347,58 @@ func buildUrl(settings Settings, endpoint string) string { // make into new string and return 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() +} diff --git a/pkg/qbittorrent/domain.go b/pkg/qbittorrent/domain.go index c13a6e3..6008410 100644 --- a/pkg/qbittorrent/domain.go +++ b/pkg/qbittorrent/domain.go @@ -50,12 +50,12 @@ type TorrentTrackersResponse 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"` Status TrackerStatus `json:"status"` NumPeers int `json:"num_peers"` NumSeeds int `json:"num_seeds"` - NumLeechers int `json:"num_leechers"` + NumLeechers int `json:"num_leeches"` NumDownloaded int `json:"num_downloaded"` Message string `json:"msg"` } diff --git a/pkg/qbittorrent/methods.go b/pkg/qbittorrent/methods.go index 6b81b10..081fbad 100644 --- a/pkg/qbittorrent/methods.go +++ b/pkg/qbittorrent/methods.go @@ -5,7 +5,7 @@ import ( "errors" "io/ioutil" "net/http" - "net/url" + "net/http/httputil" "strconv" "strings" @@ -14,11 +14,12 @@ import ( // Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication func (c *Client) Login() error { - credentials := make(map[string]string) - credentials["username"] = c.settings.Username - credentials["password"] = c.settings.Password + opts := map[string]string{ + "username": c.settings.Username, + "password": c.settings.Password, + } - resp, err := c.postBasic("auth/login", credentials) + resp, err := c.postBasic("auth/login", opts) if err != nil { log.Error().Err(err).Msg("login error") return err @@ -57,7 +58,6 @@ func (c *Client) Login() error { } func (c *Client) GetTorrents() ([]Torrent, error) { - var torrents []Torrent resp, err := c.get("torrents/info", nil) if err != nil { @@ -73,6 +73,7 @@ func (c *Client) GetTorrents() ([]Torrent, error) { return nil, readErr } + var torrents []Torrent err = json.Unmarshal(body, &torrents) if err != nil { 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) { - var torrents []Torrent + opts := map[string]string{ + "filter": string(filter), + } - v := url.Values{} - v.Add("filter", string(filter)) - params := v.Encode() - - resp, err := c.get("torrents/info?"+params, nil) + resp, err := c.get("torrents/info", opts) if err != nil { log.Error().Err(err).Msgf("get filtered torrents error: %v", filter) return nil, err @@ -103,6 +102,7 @@ func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) { return nil, readErr } + var torrents []Torrent err = json.Unmarshal(body, &torrents) if err != nil { 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) { var filter = TorrentFilterDownloading - v := url.Values{} - v.Add("filter", string(filter)) - params := v.Encode() + opts := map[string]string{ + "filter": string(filter), + } - resp, err := c.get("torrents/info?"+params, nil) + resp, err := c.get("torrents/info", opts) if err != nil { log.Error().Err(err).Msgf("get filtered torrents error: %v", filter) return nil, err @@ -167,14 +167,11 @@ func (c *Client) GetTorrentsRaw() (string, error) { } func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) { - var trackers []TorrentTracker + opts := map[string]string{ + "hash": hash, + } - params := url.Values{} - params.Add("hash", hash) - - p := params.Encode() - - resp, err := c.get("torrents/trackers?"+p, nil) + resp, err := c.get("torrents/trackers", opts) if err != nil { log.Error().Err(err).Msgf("get torrent trackers error: %v", hash) return nil, err @@ -182,12 +179,30 @@ func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) { 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) if readErr != nil { log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash) return nil, readErr } + log.Trace().Msgf("get torrent trackers body: %v", string(body)) + + var trackers []TorrentTracker err = json.Unmarshal(body, &trackers) if err != nil { 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 { - v := url.Values{} - // Add hashes together with | separator 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 { log.Error().Err(err).Msgf("delete torrents error: %v", hashes) return err @@ -239,15 +253,13 @@ func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error { } func (c *Client) ReAnnounceTorrents(hashes []string) error { - v := url.Values{} - // Add hashes together with | separator hv := strings.Join(hashes, "|") - v.Add("hashes", hv) + opts := map[string]string{ + "hashes": hv, + } - encodedHashes := v.Encode() - - resp, err := c.get("torrents/reannounce?"+encodedHashes, nil) + resp, err := c.get("torrents/reannounce", opts) if err != nil { log.Error().Err(err).Msgf("re-announce error: %v", hashes) return err @@ -262,8 +274,6 @@ func (c *Client) ReAnnounceTorrents(hashes []string) error { } func (c *Client) GetTransferInfo() (*TransferInfo, error) { - var info TransferInfo - resp, err := c.get("transfer/info", nil) if err != nil { log.Error().Err(err).Msg("get torrents error") @@ -278,6 +288,7 @@ func (c *Client) GetTransferInfo() (*TransferInfo, error) { return nil, readErr } + var info TransferInfo err = json.Unmarshal(body, &info) if err != nil { log.Error().Err(err).Msg("get torrents unmarshal error")