Skip to content

Commit

Permalink
feat(tracker): add BTN
Browse files Browse the repository at this point in the history
  • Loading branch information
s0up4200 committed Jan 15, 2025
1 parent ace7b69 commit ea3d754
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
188 changes: 188 additions & 0 deletions tracker/btn.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions tracker/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tracker

type Config struct {
BHD BHDConfig
BTN BTNConfig
PTP PTPConfig
HDB HDBConfig
RED REDConfig
Expand Down
3 changes: 3 additions & 0 deletions tracker/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down

0 comments on commit ea3d754

Please sign in to comment.