From ea3d754298d2ed534170ca396bd7a3b67a755074 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Wed, 15 Jan 2025 15:39:59 +0100 Subject: [PATCH 1/8] feat(tracker): add BTN --- README.md | 5 ++ tracker/btn.go | 188 +++++++++++++++++++++++++++++++++++++++++++++ tracker/struct.go | 1 + tracker/tracker.go | 3 + 4 files changed, 197 insertions(+) create mode 100644 tracker/btn.go diff --git a/README.md b/README.md index 76c0277..d04b779 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ filters: trackers: bhd: api_key: your-api-key + btn: + api_key: your-api-key ptp: api_user: your-api-user api_key: your-api-key @@ -136,12 +138,15 @@ Allows tqm to validate if a torrent was removed from the tracker using the track Currently implements: - Beyond-HD +- BTN - HDB - OPS - PTP - RED - UNIT3D trackers +**Note for BTN users**: When first using the BTN API, you may need to authorize your IP address. Check your BTN notices/messages for the authorization request. + ## Filtering Language Definition The language definition used in the configuration filters is available [here](https://github.com/antonmedv/expr/blob/586b86b462d22497d442adbc924bfb701db3075d/docs/Language-Definition.md) diff --git a/tracker/btn.go b/tracker/btn.go new file mode 100644 index 0000000..b907ac1 --- /dev/null +++ b/tracker/btn.go @@ -0,0 +1,188 @@ +package tracker + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "github.com/autobrr/tqm/httputils" + "github.com/autobrr/tqm/logger" + "github.com/sirupsen/logrus" + "go.uber.org/ratelimit" +) + +type BTNConfig struct { + Key string `koanf:"api_key"` +} + +type BTN struct { + cfg BTNConfig + http *http.Client + headers map[string]string + log *logrus.Entry +} + +func NewBTN(c BTNConfig) *BTN { + l := logger.GetLogger("btn-api") + return &BTN{ + cfg: c, + http: httputils.NewRetryableHttpClient(15*time.Second, ratelimit.New(1, ratelimit.WithoutSlack), l), + headers: map[string]string{ + "Accept": "application/json", + }, + log: l, + } +} + +func (c *BTN) Name() string { + return "BTN" +} + +func (c *BTN) Check(host string) bool { + return strings.EqualFold(host, "landof.tv") +} + +// extractTorrentID extracts the torrent ID from the torrent comment field +func (c *BTN) extractTorrentID(comment string) (string, error) { + if comment == "" { + return "", fmt.Errorf("empty comment field") + } + + re := regexp.MustCompile(`https?://[^/]*broadcasthe\.net/torrents\.php\?action=reqlink&id=(\d+)`) + matches := re.FindStringSubmatch(comment) + + if len(matches) < 2 { + return "", fmt.Errorf("no torrent ID found in comment: %s", comment) + } + + return matches[1], nil +} + +func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { + if !strings.EqualFold(torrent.TrackerName, "landof.tv") { + return nil, false + } + + if torrent.Comment == "" { + //c.log.Debugf("Skipping torrent check - no comment available: %s", torrent.Name) + return nil, false + } + + //c.log.Debugf("Checking torrent from %s: %s", torrent.TrackerName, torrent.Name) + + torrentID, err := c.extractTorrentID(torrent.Comment) + if err != nil { + return nil, false + } + + type JSONRPCRequest struct { + JsonRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params []interface{} `json:"params"` + ID int `json:"id"` + } + + type TorrentInfo struct { + InfoHash string `json:"InfoHash"` + ReleaseName string `json:"ReleaseName"` + } + + type TorrentsResponse struct { + Results string `json:"results"` + Torrents map[string]TorrentInfo `json:"torrents"` + } + + type JSONRPCResponse struct { + JsonRPC string `json:"jsonrpc"` + Result TorrentsResponse `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` + ID int `json:"id"` + } + + // prepare request + reqBody := JSONRPCRequest{ + JsonRPC: "2.0", + Method: "getTorrentsSearch", + Params: []interface{}{c.cfg.Key, map[string]interface{}{"id": torrentID}, 1}, + ID: 1, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + c.log.WithError(err).Error("Failed to marshal request body") + return fmt.Errorf("btn: marshal request: %w", err), false + } + + // create request + req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) + if err != nil { + c.log.WithError(err).Error("Failed to create request") + return fmt.Errorf("btn: create request: %w", err), false + } + + // set headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + // send request + resp, err := c.http.Do(req) + if err != nil { + c.log.WithError(err).Errorf("Failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) + return fmt.Errorf("btn: request check: %w", err), false + } + defer resp.Body.Close() + + // if we get a 404 or any error response, the torrent is likely unregistered + if resp.StatusCode != http.StatusOK { + return nil, true + } + + // decode response + var response JSONRPCResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", + torrent.Name, torrent.Hash) + return fmt.Errorf("btn: decode response: %w", err), false + } + + // check for RPC error + if response.Error != nil { + // check message content for IP authorization + if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { + c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") + return fmt.Errorf("btn: IP authorization required - check BTN notices"), false + } + + // default error case + c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) + return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false + } + + // check if we got any results + if response.Result.Results == "0" || len(response.Result.Torrents) == 0 { + return nil, true + } + + // compare infohash + for _, t := range response.Result.Torrents { + if strings.EqualFold(t.InfoHash, torrent.Hash) { + c.log.Debugf("Found matching torrent: %s", t.ReleaseName) + return nil, false + } + } + + // if we get here, the torrent ID exists but hash doesn't match + c.log.Debugf("Torrent ID exists but hash mismatch for: %s", torrent.Name) + return nil, true +} + +func (c *BTN) IsTrackerDown(torrent *Torrent) (error, bool) { + return nil, false +} diff --git a/tracker/struct.go b/tracker/struct.go index 6476aa6..148c04f 100644 --- a/tracker/struct.go +++ b/tracker/struct.go @@ -2,6 +2,7 @@ package tracker type Config struct { BHD BHDConfig + BTN BTNConfig PTP PTPConfig HDB HDBConfig RED REDConfig diff --git a/tracker/tracker.go b/tracker/tracker.go index 29c7a2b..9cb3e7a 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -11,6 +11,9 @@ func Init(cfg Config) error { if cfg.BHD.Key != "" { trackers = append(trackers, NewBHD(cfg.BHD)) } + if cfg.BTN.Key != "" { + trackers = append(trackers, NewBTN(cfg.BTN)) + } if cfg.PTP.User != "" && cfg.PTP.Key != "" { trackers = append(trackers, NewPTP(cfg.PTP)) } From 7f7e194dc8ab0410d396f9ede8d05dc54c095764 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 12:59:07 +0100 Subject: [PATCH 2/8] fix(btn): return error on non-200 status codes instead of assuming unregistered --- tracker/btn.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index b907ac1..51d070c 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -139,9 +139,8 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { } defer resp.Body.Close() - // if we get a 404 or any error response, the torrent is likely unregistered if resp.StatusCode != http.StatusOK { - return nil, true + return fmt.Errorf("btn: unexpected status code: %d", resp.StatusCode), false } // decode response From c30fe94d9ab284ea59b828b925435b12621fd813 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 13:03:50 +0100 Subject: [PATCH 3/8] refactor: cleanup --- tracker/btn.go | 52 +++++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index 51d070c..6dc3d32 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -62,21 +62,18 @@ func (c *BTN) extractTorrentID(comment string) (string, error) { return matches[1], nil } -func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { +func (c *BTN) IsUnregistered(torrent *Torrent) (bool, error) { if !strings.EqualFold(torrent.TrackerName, "landof.tv") { - return nil, false + return false, nil } if torrent.Comment == "" { - //c.log.Debugf("Skipping torrent check - no comment available: %s", torrent.Name) - return nil, false + return false, nil } - //c.log.Debugf("Checking torrent from %s: %s", torrent.TrackerName, torrent.Name) - torrentID, err := c.extractTorrentID(torrent.Comment) if err != nil { - return nil, false + return false, nil } type JSONRPCRequest struct { @@ -116,15 +113,15 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { jsonBody, err := json.Marshal(reqBody) if err != nil { - c.log.WithError(err).Error("Failed to marshal request body") - return fmt.Errorf("btn: marshal request: %w", err), false + c.log.WithError(err).Error("failed to marshal request body") + return false, fmt.Errorf("marshal request: %w", err) } // create request req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) if err != nil { - c.log.WithError(err).Error("Failed to create request") - return fmt.Errorf("btn: create request: %w", err), false + c.log.WithError(err).Error("failed to create request") + return false, fmt.Errorf("create request: %w", err) } // set headers @@ -134,54 +131,53 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // send request resp, err := c.http.Do(req) if err != nil { - c.log.WithError(err).Errorf("Failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) - return fmt.Errorf("btn: request check: %w", err), false + c.log.WithError(err).Errorf("failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) + return false, fmt.Errorf("request check: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("btn: unexpected status code: %d", resp.StatusCode), false + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } // decode response var response JSONRPCResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", - torrent.Name, torrent.Hash) - return fmt.Errorf("btn: decode response: %w", err), false + c.log.WithError(err).Errorf("failed decoding response for %s (hash: %s)", torrent.Name, torrent.Hash) + return false, fmt.Errorf("decode response: %w", err) } // check for RPC error if response.Error != nil { // check message content for IP authorization if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { - c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") - return fmt.Errorf("btn: IP authorization required - check BTN notices"), false + c.log.Error("api requires ip authorization - check btn notices") + return false, fmt.Errorf("ip authorization required") } // default error case - c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) - return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false + c.log.WithError(fmt.Errorf(response.Error.Message)).Errorf("api error (code: %d)", response.Error.Code) + return false, fmt.Errorf("api error: %s (code: %d)", response.Error.Message, response.Error.Code) } // check if we got any results if response.Result.Results == "0" || len(response.Result.Torrents) == 0 { - return nil, true + return true, nil } // compare infohash for _, t := range response.Result.Torrents { if strings.EqualFold(t.InfoHash, torrent.Hash) { - c.log.Debugf("Found matching torrent: %s", t.ReleaseName) - return nil, false + c.log.Debugf("found matching torrent: %s", t.ReleaseName) + return false, nil } } // if we get here, the torrent ID exists but hash doesn't match - c.log.Debugf("Torrent ID exists but hash mismatch for: %s", torrent.Name) - return nil, true + c.log.Debugf("torrent id exists but hash mismatch for: %s", torrent.Name) + return true, nil } -func (c *BTN) IsTrackerDown(torrent *Torrent) (error, bool) { - return nil, false +func (c *BTN) IsTrackerDown(torrent *Torrent) (bool, error) { + return false, nil } From 700d249503468fe29c015727dfa0dfbd5bc03ec6 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 13:09:11 +0100 Subject: [PATCH 4/8] Revert "refactor: cleanup" This reverts commit c30fe94d9ab284ea59b828b925435b12621fd813. --- tracker/btn.go | 52 +++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index 6dc3d32..51d070c 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -62,18 +62,21 @@ func (c *BTN) extractTorrentID(comment string) (string, error) { return matches[1], nil } -func (c *BTN) IsUnregistered(torrent *Torrent) (bool, error) { +func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { if !strings.EqualFold(torrent.TrackerName, "landof.tv") { - return false, nil + return nil, false } if torrent.Comment == "" { - return false, nil + //c.log.Debugf("Skipping torrent check - no comment available: %s", torrent.Name) + return nil, false } + //c.log.Debugf("Checking torrent from %s: %s", torrent.TrackerName, torrent.Name) + torrentID, err := c.extractTorrentID(torrent.Comment) if err != nil { - return false, nil + return nil, false } type JSONRPCRequest struct { @@ -113,15 +116,15 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (bool, error) { jsonBody, err := json.Marshal(reqBody) if err != nil { - c.log.WithError(err).Error("failed to marshal request body") - return false, fmt.Errorf("marshal request: %w", err) + c.log.WithError(err).Error("Failed to marshal request body") + return fmt.Errorf("btn: marshal request: %w", err), false } // create request req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) if err != nil { - c.log.WithError(err).Error("failed to create request") - return false, fmt.Errorf("create request: %w", err) + c.log.WithError(err).Error("Failed to create request") + return fmt.Errorf("btn: create request: %w", err), false } // set headers @@ -131,53 +134,54 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (bool, error) { // send request resp, err := c.http.Do(req) if err != nil { - c.log.WithError(err).Errorf("failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) - return false, fmt.Errorf("request check: %w", err) + c.log.WithError(err).Errorf("Failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) + return fmt.Errorf("btn: request check: %w", err), false } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return fmt.Errorf("btn: unexpected status code: %d", resp.StatusCode), false } // decode response var response JSONRPCResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - c.log.WithError(err).Errorf("failed decoding response for %s (hash: %s)", torrent.Name, torrent.Hash) - return false, fmt.Errorf("decode response: %w", err) + c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", + torrent.Name, torrent.Hash) + return fmt.Errorf("btn: decode response: %w", err), false } // check for RPC error if response.Error != nil { // check message content for IP authorization if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { - c.log.Error("api requires ip authorization - check btn notices") - return false, fmt.Errorf("ip authorization required") + c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") + return fmt.Errorf("btn: IP authorization required - check BTN notices"), false } // default error case - c.log.WithError(fmt.Errorf(response.Error.Message)).Errorf("api error (code: %d)", response.Error.Code) - return false, fmt.Errorf("api error: %s (code: %d)", response.Error.Message, response.Error.Code) + c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) + return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false } // check if we got any results if response.Result.Results == "0" || len(response.Result.Torrents) == 0 { - return true, nil + return nil, true } // compare infohash for _, t := range response.Result.Torrents { if strings.EqualFold(t.InfoHash, torrent.Hash) { - c.log.Debugf("found matching torrent: %s", t.ReleaseName) - return false, nil + c.log.Debugf("Found matching torrent: %s", t.ReleaseName) + return nil, false } } // if we get here, the torrent ID exists but hash doesn't match - c.log.Debugf("torrent id exists but hash mismatch for: %s", torrent.Name) - return true, nil + c.log.Debugf("Torrent ID exists but hash mismatch for: %s", torrent.Name) + return nil, true } -func (c *BTN) IsTrackerDown(torrent *Torrent) (bool, error) { - return false, nil +func (c *BTN) IsTrackerDown(torrent *Torrent) (error, bool) { + return nil, false } From 5fe6473bb83d12975d2daa2491367bcc46e816b1 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 13:30:30 +0100 Subject: [PATCH 5/8] refactor(btn): cleanup --- tracker/btn.go | 48 +++++++++++------------------------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index 51d070c..fc2531f 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -20,10 +20,9 @@ type BTNConfig struct { } type BTN struct { - cfg BTNConfig - http *http.Client - headers map[string]string - log *logrus.Entry + cfg BTNConfig + http *http.Client + log *logrus.Entry } func NewBTN(c BTNConfig) *BTN { @@ -31,10 +30,7 @@ func NewBTN(c BTNConfig) *BTN { return &BTN{ cfg: c, http: httputils.NewRetryableHttpClient(15*time.Second, ratelimit.New(1, ratelimit.WithoutSlack), l), - headers: map[string]string{ - "Accept": "application/json", - }, - log: l, + log: l, } } @@ -63,17 +59,10 @@ func (c *BTN) extractTorrentID(comment string) (string, error) { } func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { - if !strings.EqualFold(torrent.TrackerName, "landof.tv") { + if !strings.EqualFold(torrent.TrackerName, "landof.tv") || torrent.Comment == "" { return nil, false } - if torrent.Comment == "" { - //c.log.Debugf("Skipping torrent check - no comment available: %s", torrent.Name) - return nil, false - } - - //c.log.Debugf("Checking torrent from %s: %s", torrent.TrackerName, torrent.Name) - torrentID, err := c.extractTorrentID(torrent.Comment) if err != nil { return nil, false @@ -91,15 +80,13 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { ReleaseName string `json:"ReleaseName"` } - type TorrentsResponse struct { - Results string `json:"results"` - Torrents map[string]TorrentInfo `json:"torrents"` - } - type JSONRPCResponse struct { - JsonRPC string `json:"jsonrpc"` - Result TorrentsResponse `json:"result"` - Error *struct { + JsonRPC string `json:"jsonrpc"` + Result struct { + Results string `json:"results"` + Torrents map[string]TorrentInfo `json:"torrents"` + } `json:"result"` + Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error,omitempty"` @@ -116,25 +103,21 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { jsonBody, err := json.Marshal(reqBody) if err != nil { - c.log.WithError(err).Error("Failed to marshal request body") return fmt.Errorf("btn: marshal request: %w", err), false } // create request req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) if err != nil { - c.log.WithError(err).Error("Failed to create request") return fmt.Errorf("btn: create request: %w", err), false } - // set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") // send request resp, err := c.http.Do(req) if err != nil { - c.log.WithError(err).Errorf("Failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) return fmt.Errorf("btn: request check: %w", err), false } defer resp.Body.Close() @@ -146,8 +129,6 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // decode response var response JSONRPCResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", - torrent.Name, torrent.Hash) return fmt.Errorf("btn: decode response: %w", err), false } @@ -155,12 +136,8 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { if response.Error != nil { // check message content for IP authorization if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { - c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") return fmt.Errorf("btn: IP authorization required - check BTN notices"), false } - - // default error case - c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false } @@ -172,13 +149,10 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // compare infohash for _, t := range response.Result.Torrents { if strings.EqualFold(t.InfoHash, torrent.Hash) { - c.log.Debugf("Found matching torrent: %s", t.ReleaseName) return nil, false } } - // if we get here, the torrent ID exists but hash doesn't match - c.log.Debugf("Torrent ID exists but hash mismatch for: %s", torrent.Name) return nil, true } From 5b4a068f43653ed0fbe5569793d661064540b54c Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 20:33:47 +0100 Subject: [PATCH 6/8] fix: bring back c.log lines --- tracker/btn.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tracker/btn.go b/tracker/btn.go index fc2531f..67848c5 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -103,21 +103,25 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { jsonBody, err := json.Marshal(reqBody) if err != nil { + c.log.WithError(err).Error("Failed to marshal request body") return fmt.Errorf("btn: marshal request: %w", err), false } // create request req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) if err != nil { + c.log.WithError(err).Error("Failed to create request") return fmt.Errorf("btn: create request: %w", err), false } + // set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") // send request resp, err := c.http.Do(req) if err != nil { + c.log.WithError(err).Errorf("Failed checking torrent %s (hash: %s)", torrent.Name, torrent.Hash) return fmt.Errorf("btn: request check: %w", err), false } defer resp.Body.Close() @@ -129,6 +133,8 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // decode response var response JSONRPCResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", + torrent.Name, torrent.Hash) return fmt.Errorf("btn: decode response: %w", err), false } @@ -136,8 +142,12 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { if response.Error != nil { // check message content for IP authorization if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { + c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") return fmt.Errorf("btn: IP authorization required - check BTN notices"), false } + + // default error case + c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false } @@ -149,10 +159,13 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // compare infohash for _, t := range response.Result.Torrents { if strings.EqualFold(t.InfoHash, torrent.Hash) { + c.log.Debugf("Found matching torrent: %s", t.ReleaseName) return nil, false } } + // if we get here, the torrent ID exists but hash doesn't match + c.log.Debugf("Torrent ID exists but hash mismatch for: %s", torrent.Name) return nil, true } From 1f9d100d5efdab7e5c348871939ef9c36a1fcb76 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Fri, 17 Jan 2025 20:36:38 +0100 Subject: [PATCH 7/8] final cleanup --- tracker/btn.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index 67848c5..cda171c 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -103,14 +103,12 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { jsonBody, err := json.Marshal(reqBody) if err != nil { - c.log.WithError(err).Error("Failed to marshal request body") return fmt.Errorf("btn: marshal request: %w", err), false } // create request req, err := http.NewRequest(http.MethodPost, "https://api.broadcasthe.net", bytes.NewReader(jsonBody)) if err != nil { - c.log.WithError(err).Error("Failed to create request") return fmt.Errorf("btn: create request: %w", err), false } @@ -133,8 +131,6 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // decode response var response JSONRPCResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - c.log.WithError(err).Errorf("Failed decoding response for %s (hash: %s)", - torrent.Name, torrent.Hash) return fmt.Errorf("btn: decode response: %w", err), false } @@ -142,12 +138,11 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { if response.Error != nil { // check message content for IP authorization if strings.Contains(strings.ToLower(response.Error.Message), "ip address needs authorization") { - c.log.Errorf("BTN API requires IP authorization. Please check your notices on BTN") + c.log.Error("BTN API requires IP authorization. Please check your notices on BTN") return fmt.Errorf("btn: IP authorization required - check BTN notices"), false } // default error case - c.log.WithError(fmt.Errorf("%s", response.Error.Message)).Errorf("API error (code: %d)", response.Error.Code) return fmt.Errorf("btn: api error: %s (code: %d)", response.Error.Message, response.Error.Code), false } @@ -159,7 +154,6 @@ func (c *BTN) IsUnregistered(torrent *Torrent) (error, bool) { // compare infohash for _, t := range response.Result.Torrents { if strings.EqualFold(t.InfoHash, torrent.Hash) { - c.log.Debugf("Found matching torrent: %s", t.ReleaseName) return nil, false } } From a04b74b26fb338552bfd66b1b50d5acc11f91891 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Thu, 6 Feb 2025 20:09:12 +0100 Subject: [PATCH 8/8] refactor: compile regex once per run --- tracker/btn.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tracker/btn.go b/tracker/btn.go index cda171c..48b3d04 100644 --- a/tracker/btn.go +++ b/tracker/btn.go @@ -15,6 +15,8 @@ import ( "go.uber.org/ratelimit" ) +var torrentIDRegex = regexp.MustCompile(`https?://[^/]*broadcasthe\.net/torrents\.php\?action=reqlink&id=(\d+)`) + type BTNConfig struct { Key string `koanf:"api_key"` } @@ -48,8 +50,7 @@ func (c *BTN) extractTorrentID(comment string) (string, error) { return "", fmt.Errorf("empty comment field") } - re := regexp.MustCompile(`https?://[^/]*broadcasthe\.net/torrents\.php\?action=reqlink&id=(\d+)`) - matches := re.FindStringSubmatch(comment) + matches := torrentIDRegex.FindStringSubmatch(comment) if len(matches) < 2 { return "", fmt.Errorf("no torrent ID found in comment: %s", comment)