Add feature to remove torrent tracker URLs from torrents for private tracker downloads (#99)

- Remove trackers from torrenst/magnet URI

---------

Co-authored-by: Mukhtar Akere <akeremukhtar10@gmail.com>
This commit is contained in:
crashxer
2025-10-22 09:44:23 -06:00
committed by GitHub
parent 7032cc368b
commit 7af90ebe47
25 changed files with 525 additions and 74 deletions

View File

@@ -49,14 +49,15 @@ type Debrid struct {
}
type QBitTorrent struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Port string `json:"port,omitempty"` // deprecated
DownloadFolder string `json:"download_folder,omitempty"`
Categories []string `json:"categories,omitempty"`
RefreshInterval int `json:"refresh_interval,omitempty"`
SkipPreCache bool `json:"skip_pre_cache,omitempty"`
MaxDownloads int `json:"max_downloads,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Port string `json:"port,omitempty"` // deprecated
DownloadFolder string `json:"download_folder,omitempty"`
Categories []string `json:"categories,omitempty"`
RefreshInterval int `json:"refresh_interval,omitempty"`
SkipPreCache bool `json:"skip_pre_cache,omitempty"`
MaxDownloads int `json:"max_downloads,omitempty"`
AlwaysRmTrackerUrls bool `json:"always_rm_tracker_urls,omitempty"`
}
type Arr struct {

View File

@@ -0,0 +1,45 @@
package testutil
import (
"os"
"path/filepath"
"strings"
)
// GetTestDataPath returns the path to the testdata directory in the project root
func GetTestDataPath() string {
return filepath.Join("..", "..", "testdata")
}
// GetTestDataFilePath returns the path to a specific file in the testdata directory
func GetTestDataFilePath(filename string) string {
return filepath.Join(GetTestDataPath(), filename)
}
// GetTestTorrentPath returns the path to the Ubuntu test torrent file
func GetTestTorrentPath() string {
return GetTestDataFilePath("ubuntu-25.04-desktop-amd64.iso.torrent")
}
// GetTestMagnetPath returns the path to the Ubuntu test magnet file
func GetTestMagnetPath() string {
return GetTestDataFilePath("ubuntu-25.04-desktop-amd64.iso.magnet")
}
// GetTestDataBytes reads and returns the raw bytes of a test data file
func GetTestDataBytes(filename string) ([]byte, error) {
filePath := GetTestDataFilePath(filename)
return os.ReadFile(filePath)
}
// GetTestDataContent reads and returns the content of a test data file
func GetTestDataContent(filename string) (string, error) {
content, err := GetTestDataBytes(filename)
return strings.TrimSpace(string(content)), err
}
// GetTestMagnetContent reads and returns the content of the Ubuntu test magnet file
func GetTestMagnetContent() (string, error) {
return GetTestDataContent("ubuntu-25.04-desktop-amd64.iso.magnet")
}

View File

@@ -7,8 +7,6 @@ import (
"encoding/base32"
"encoding/hex"
"fmt"
"github.com/anacrolix/torrent/metainfo"
"github.com/sirrobot01/decypharr/internal/request"
"io"
"log"
"net/http"
@@ -18,6 +16,9 @@ import (
"regexp"
"strings"
"time"
"github.com/anacrolix/torrent/metainfo"
"github.com/sirrobot01/decypharr/internal/request"
)
var (
@@ -36,7 +37,17 @@ func (m *Magnet) IsTorrent() bool {
return m.File != nil
}
func GetMagnetFromFile(file io.Reader, filePath string) (*Magnet, error) {
// stripTrackersFromMagnet removes trackers from a magnet and returns a modified copy
func stripTrackersFromMagnet(mi metainfo.Magnet, fileType string) metainfo.Magnet {
originalTrackerCount := len(mi.Trackers)
if len(mi.Trackers) > 0 {
mi.Trackers = nil
log.Printf("Removed %d tracker URLs from %s", originalTrackerCount, fileType)
}
return mi
}
func GetMagnetFromFile(file io.Reader, filePath string, rmTrackerUrls bool) (*Magnet, error) {
var (
m *Magnet
err error
@@ -46,14 +57,14 @@ func GetMagnetFromFile(file io.Reader, filePath string) (*Magnet, error) {
if err != nil {
return nil, err
}
m, err = GetMagnetFromBytes(torrentData)
m, err = GetMagnetFromBytes(torrentData, rmTrackerUrls)
if err != nil {
return nil, err
}
} else {
// .magnet file
magnetLink := ReadMagnetFile(file)
m, err = GetMagnetInfo(magnetLink)
m, err = GetMagnetInfo(magnetLink, rmTrackerUrls)
if err != nil {
return nil, err
}
@@ -62,32 +73,37 @@ func GetMagnetFromFile(file io.Reader, filePath string) (*Magnet, error) {
return m, nil
}
func GetMagnetFromUrl(url string) (*Magnet, error) {
func GetMagnetFromUrl(url string, rmTrackerUrls bool) (*Magnet, error) {
if strings.HasPrefix(url, "magnet:") {
return GetMagnetInfo(url)
return GetMagnetInfo(url, rmTrackerUrls)
} else if strings.HasPrefix(url, "http") {
return OpenMagnetHttpURL(url)
return OpenMagnetHttpURL(url, rmTrackerUrls)
}
return nil, fmt.Errorf("invalid url")
}
func GetMagnetFromBytes(torrentData []byte) (*Magnet, error) {
func GetMagnetFromBytes(torrentData []byte, rmTrackerUrls bool) (*Magnet, error) {
// Create a scanner to read the file line by line
mi, err := metainfo.Load(bytes.NewReader(torrentData))
if err != nil {
return nil, err
}
hash := mi.HashInfoBytes()
infoHash := hash.HexString()
info, err := mi.UnmarshalInfo()
if err != nil {
return nil, err
}
magnetMeta := mi.Magnet(&hash, &info)
if rmTrackerUrls {
magnetMeta = stripTrackersFromMagnet(magnetMeta, "torrent file")
}
magnet := &Magnet{
InfoHash: infoHash,
Name: info.Name,
Size: info.Length,
Link: mi.Magnet(&hash, &info).String(),
Link: magnetMeta.String(),
File: torrentData,
}
return magnet, nil
@@ -124,7 +140,7 @@ func ReadMagnetFile(file io.Reader) string {
return ""
}
func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) {
func OpenMagnetHttpURL(magnetLink string, rmTrackerUrls bool) (*Magnet, error) {
resp, err := http.Get(magnetLink)
if err != nil {
return nil, fmt.Errorf("error making GET request: %v", err)
@@ -139,34 +155,35 @@ func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) {
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
return GetMagnetFromBytes(torrentData)
return GetMagnetFromBytes(torrentData, rmTrackerUrls)
}
func GetMagnetInfo(magnetLink string) (*Magnet, error) {
func GetMagnetInfo(magnetLink string, rmTrackerUrls bool) (*Magnet, error) {
if magnetLink == "" {
return nil, fmt.Errorf("error getting magnet from file")
}
magnetURI, err := url.Parse(magnetLink)
mi, err := metainfo.ParseMagnetUri(magnetLink)
if err != nil {
return nil, fmt.Errorf("error parsing magnet link")
return nil, fmt.Errorf("error parsing magnet link: %w", err)
}
query := magnetURI.Query()
xt := query.Get("xt")
dn := query.Get("dn")
// Extract BTIH
parts := strings.Split(xt, ":")
btih := ""
if len(parts) > 2 {
btih = parts[2]
// Strip all announce URLs if requested
if rmTrackerUrls {
mi = stripTrackersFromMagnet(mi, "magnet link")
}
btih := mi.InfoHash.HexString()
dn := mi.DisplayName
// Reconstruct the magnet link using the (possibly modified) spec
finalLink := mi.String()
magnet := &Magnet{
InfoHash: btih,
Name: dn,
Size: 0,
Link: magnetLink,
Link: finalLink,
}
return magnet, nil
}

View File

@@ -0,0 +1,198 @@
package utils
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/sirrobot01/decypharr/internal/testutil"
)
// checkMagnet is a helper function that verifies magnet properties
func checkMagnet(t *testing.T, magnet *Magnet, expectedInfoHash, expectedName, expectedLink string, expectedTrackerCount int, shouldBeTorrent bool) {
t.Helper() // This marks the function as a test helper
// Verify basic properties
if magnet.Name != expectedName {
t.Errorf("Expected name '%s', got '%s'", expectedName, magnet.Name)
}
if magnet.InfoHash != expectedInfoHash {
t.Errorf("Expected InfoHash '%s', got '%s'", expectedInfoHash, magnet.InfoHash)
}
if magnet.Link != expectedLink {
t.Errorf("Expected Link '%s', got '%s'", expectedLink, magnet.Link)
}
// Verify the magnet link contains the essential info hash
if !strings.Contains(magnet.Link, "xt=urn:btih:"+expectedInfoHash) {
t.Error("Magnet link should contain info hash")
}
// Verify tracker count
trCount := strings.Count(magnet.Link, "tr=")
if trCount != expectedTrackerCount {
t.Errorf("Expected %d tracker URLs, got %d", expectedTrackerCount, trCount)
}
}
// testMagnetFromFile is a helper function for tests that use GetMagnetFromFile with file operations
func testMagnetFromFile(t *testing.T, filePath string, rmTrackerUrls bool, expectedInfoHash, expectedName, expectedLink string, expectedTrackerCount int) {
t.Helper()
file, err := os.Open(filePath)
if err != nil {
t.Fatalf("Failed to open torrent file %s: %v", filePath, err)
}
defer file.Close()
magnet, err := GetMagnetFromFile(file, filepath.Base(filePath), rmTrackerUrls)
if err != nil {
t.Fatalf("GetMagnetFromFile failed: %v", err)
}
checkMagnet(t, magnet, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount, true)
// Log the result
if rmTrackerUrls {
t.Logf("Generated clean magnet link: %s", magnet.Link)
} else {
t.Logf("Generated magnet link with trackers: %s", magnet.Link)
}
}
func TestGetMagnetFromFile_RealTorrentFile_StripTrue(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso"
expectedTrackerCount := 0 // Should be 0 when stripping trackers
torrentPath := testutil.GetTestTorrentPath()
testMagnetFromFile(t, torrentPath, true, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}
func TestGetMagnetFromFile_RealTorrentFile_StripFalse(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso&tr=https%3A%2F%2Ftorrent.ubuntu.com%2Fannounce&tr=https%3A%2F%2Fipv6.torrent.ubuntu.com%2Fannounce"
expectedTrackerCount := 2 // Should be 2 when preserving trackers
torrentPath := testutil.GetTestTorrentPath()
testMagnetFromFile(t, torrentPath, false, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}
func TestGetMagnetFromFile_MagnetFile_StripTrue(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso"
expectedTrackerCount := 0 // Should be 0 when stripping trackers
torrentPath := testutil.GetTestMagnetPath()
testMagnetFromFile(t, torrentPath, true, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}
func TestGetMagnetFromFile_MagnetFile_StripFalse(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso&tr=https%3A%2F%2Fipv6.torrent.ubuntu.com%2Fannounce&tr=https%3A%2F%2Ftorrent.ubuntu.com%2Fannounce"
expectedTrackerCount := 2
torrentPath := testutil.GetTestMagnetPath()
testMagnetFromFile(t, torrentPath, false, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}
func TestGetMagnetFromUrl_MagnetLink_StripTrue(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso"
expectedTrackerCount := 0
// Load the magnet URL from the test file
magnetUrl, err := testutil.GetTestMagnetContent()
if err != nil {
t.Fatalf("Failed to load magnet URL from test file: %v", err)
}
magnet, err := GetMagnetFromUrl(magnetUrl, true)
if err != nil {
t.Fatalf("GetMagnetFromUrl failed: %v", err)
}
checkMagnet(t, magnet, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount, false)
t.Logf("Generated clean magnet link: %s", magnet.Link)
}
func TestGetMagnetFromUrl_MagnetLink_StripFalse(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso&tr=https%3A%2F%2Fipv6.torrent.ubuntu.com%2Fannounce&tr=https%3A%2F%2Ftorrent.ubuntu.com%2Fannounce"
expectedTrackerCount := 2
// Load the magnet URL from the test file
magnetUrl, err := testutil.GetTestMagnetContent()
if err != nil {
t.Fatalf("Failed to load magnet URL from test file: %v", err)
}
magnet, err := GetMagnetFromUrl(magnetUrl, false)
if err != nil {
t.Fatalf("GetMagnetFromUrl failed: %v", err)
}
checkMagnet(t, magnet, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount, false)
t.Logf("Generated magnet link with trackers: %s", magnet.Link)
}
// testMagnetFromHttpTorrent is a helper function for tests that use GetMagnetFromUrl with HTTP torrent links
func testMagnetFromHttpTorrent(t *testing.T, torrentPath string, rmTrackerUrls bool, expectedInfoHash, expectedName, expectedLink string, expectedTrackerCount int) {
t.Helper()
// Read the torrent file content
torrentData, err := testutil.GetTestDataBytes(torrentPath)
if err != nil {
t.Fatalf("Failed to read torrent file: %v", err)
}
// Create a test HTTP server that serves the torrent file
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-bittorrent")
w.Write(torrentData)
}))
defer server.Close()
// Test the function with the mock server URL
magnet, err := GetMagnetFromUrl(server.URL, rmTrackerUrls)
if err != nil {
t.Fatalf("GetMagnetFromUrl failed: %v", err)
}
checkMagnet(t, magnet, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount, true)
// Log the result
if rmTrackerUrls {
t.Logf("Generated clean magnet link from HTTP torrent: %s", magnet.Link)
} else {
t.Logf("Generated magnet link with trackers from HTTP torrent: %s", magnet.Link)
}
}
func TestGetMagnetFromUrl_TorrentLink_StripTrue(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso"
expectedTrackerCount := 0
testMagnetFromHttpTorrent(t, "ubuntu-25.04-desktop-amd64.iso.torrent", true, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}
func TestGetMagnetFromUrl_TorrentLink_StripFalse(t *testing.T) {
expectedInfoHash := "8a19577fb5f690970ca43a57ff1011ae202244b8"
expectedName := "ubuntu-25.04-desktop-amd64.iso"
expectedLink := "magnet:?xt=urn:btih:8a19577fb5f690970ca43a57ff1011ae202244b8&dn=ubuntu-25.04-desktop-amd64.iso&tr=https%3A%2F%2Ftorrent.ubuntu.com%2Fannounce&tr=https%3A%2F%2Fipv6.torrent.ubuntu.com%2Fannounce"
expectedTrackerCount := 2
testMagnetFromHttpTorrent(t, "ubuntu-25.04-desktop-amd64.iso.torrent", false, expectedInfoHash, expectedName, expectedLink, expectedTrackerCount)
}