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:
@@ -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 {
|
||||
|
||||
45
internal/testutil/testutil.go
Normal file
45
internal/testutil/testutil.go
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
198
internal/utils/magnet_test.go
Normal file
198
internal/utils/magnet_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user