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
+5 -1
View File
@@ -184,7 +184,11 @@ func (c *Cache) MarkLinkAsInvalid(downloadLink types.DownloadLink, reason string
c.logger.Error().Str("token", utils.Mask(downloadLink.Token)).Msg("Account not found to delete download link")
return
}
c.client.DeleteDownloadLink(account, downloadLink)
if err := c.client.DeleteDownloadLink(account, downloadLink); err != nil {
c.logger.Error().Err(err).Str("token", utils.Mask(downloadLink.Token)).Msg("Failed to delete download link from account")
return
}
}
}
+4 -7
View File
@@ -104,9 +104,6 @@ func (c *Cache) Stream(ctx context.Context, start, end int64, linkFunc func() (t
}
if streamErr.LinkError {
c.logger.Trace().
Int("retries", retry).
Msg("Link error, getting fresh link")
lastErr = streamErr
// Try new link
downloadLink, err = linkFunc()
@@ -116,7 +113,7 @@ func (c *Cache) Stream(ctx context.Context, start, end int64, linkFunc func() (t
continue
}
// Retryable HTTP error (429, 503, etc.) - retry network
// Retryable HTTP error (429, 503, 404 etc.) - retry network
lastErr = streamErr
c.logger.Trace().
Err(lastErr).
@@ -198,9 +195,6 @@ func (c *Cache) doRequest(ctx context.Context, url string, start, end int64) (*h
}
func (c *Cache) handleHTTPError(resp *http.Response, downloadLink types.DownloadLink) StreamError {
body, _ := io.ReadAll(resp.Body)
bodyStr := strings.ToLower(string(body))
switch resp.StatusCode {
case http.StatusNotFound:
c.MarkLinkAsInvalid(downloadLink, "link_not_found")
@@ -211,6 +205,8 @@ func (c *Cache) handleHTTPError(resp *http.Response, downloadLink types.Download
}
case http.StatusServiceUnavailable:
body, _ := io.ReadAll(resp.Body)
bodyStr := strings.ToLower(string(body))
if strings.Contains(bodyStr, "bandwidth") || strings.Contains(bodyStr, "traffic") {
c.MarkLinkAsInvalid(downloadLink, "bandwidth_exceeded")
return StreamError{
@@ -230,6 +226,7 @@ func (c *Cache) handleHTTPError(resp *http.Response, downloadLink types.Download
default:
retryable := resp.StatusCode >= 500
body, _ := io.ReadAll(resp.Body)
return StreamError{
Err: fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)),
Retryable: retryable,
+9 -2
View File
@@ -102,6 +102,13 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
if strings.ToLower(r.FormValue("sequentialDownload")) == "true" {
action = "download"
}
rmTrackerUrls := strings.ToLower(r.FormValue("firstLastPiecePrio")) == "true"
// Check config setting - if always remove tracker URLs is enabled, force it to true
if q.AlwaysRmTrackerUrls {
rmTrackerUrls = true
}
debridName := r.FormValue("debrid")
category := r.FormValue("category")
_arr := getArrFromContext(ctx)
@@ -118,7 +125,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
urlList = append(urlList, strings.TrimSpace(u))
}
for _, url := range urlList {
if err := q.addMagnet(ctx, url, _arr, debridName, action); err != nil {
if err := q.addMagnet(ctx, url, _arr, debridName, action, rmTrackerUrls); err != nil {
q.logger.Debug().Msgf("Error adding magnet: %s", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -131,7 +138,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
if r.MultipartForm != nil && r.MultipartForm.File != nil {
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
for _, fileHeader := range files {
if err := q.addTorrent(ctx, fileHeader, _arr, debridName, action); err != nil {
if err := q.addTorrent(ctx, fileHeader, _arr, debridName, action, rmTrackerUrls); err != nil {
q.logger.Debug().Err(err).Msgf("Error adding torrent")
http.Error(w, err.Error(), http.StatusBadRequest)
return
+15 -13
View File
@@ -8,25 +8,27 @@ import (
)
type QBit struct {
Username string
Password string
DownloadFolder string
Categories []string
storage *wire.TorrentStorage
logger zerolog.Logger
Tags []string
Username string
Password string
DownloadFolder string
Categories []string
AlwaysRmTrackerUrls bool
storage *wire.TorrentStorage
logger zerolog.Logger
Tags []string
}
func New() *QBit {
_cfg := config.Get()
cfg := _cfg.QBitTorrent
return &QBit{
Username: cfg.Username,
Password: cfg.Password,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
storage: wire.Get().Torrents(),
logger: logger.New("qbit"),
Username: cfg.Username,
Password: cfg.Password,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
AlwaysRmTrackerUrls: cfg.AlwaysRmTrackerUrls,
storage: wire.Get().Torrents(),
logger: logger.New("qbit"),
}
}
+4 -4
View File
@@ -14,8 +14,8 @@ import (
)
// All torrent-related helpers goes here
func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid string, action string) error {
magnet, err := utils.GetMagnetFromUrl(url)
func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid string, action string, rmTrackerUrls bool) error {
magnet, err := utils.GetMagnetFromUrl(url, rmTrackerUrls)
if err != nil {
return fmt.Errorf("error parsing magnet link: %w", err)
}
@@ -30,11 +30,11 @@ func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid s
return nil
}
func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader, arr *arr.Arr, debrid string, action string) error {
func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader, arr *arr.Arr, debrid string, action string, rmTrackerUrls bool) error {
file, _ := fileHeader.Open()
defer file.Close()
var reader io.Reader = file
magnet, err := utils.GetMagnetFromFile(reader, fileHeader.Filename)
magnet, err := utils.GetMagnetFromFile(reader, fileHeader.Filename, rmTrackerUrls)
if err != nil {
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
}
+9 -2
View File
@@ -46,6 +46,13 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
skipMultiSeason := r.FormValue("skipMultiSeason") == "true"
downloadUncached := r.FormValue("downloadUncached") == "true"
rmTrackerUrls := r.FormValue("rmTrackerUrls") == "true"
// Check config setting - if always remove tracker URLs is enabled, force it to true
cfg := config.Get()
if cfg.QBitTorrent.AlwaysRmTrackerUrls {
rmTrackerUrls = true
}
_arr := _store.Arr().Get(arrName)
if _arr == nil {
@@ -63,7 +70,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
}
for _, url := range urlList {
magnet, err := utils.GetMagnetFromUrl(url)
magnet, err := utils.GetMagnetFromUrl(url, rmTrackerUrls)
if err != nil {
errs = append(errs, fmt.Sprintf("Failed to parse URL %s: %v", url, err))
continue
@@ -88,7 +95,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
continue
}
magnet, err := utils.GetMagnetFromFile(file, fileHeader.Filename)
magnet, err := utils.GetMagnetFromFile(file, fileHeader.Filename, rmTrackerUrls)
if err != nil {
errs = append(errs, fmt.Sprintf("Failed to parse torrent file %s: %v", fileHeader.Filename, err))
continue
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -114,7 +114,7 @@ class ConfigManager {
populateQBittorrentSettings(qbitConfig) {
if (!qbitConfig) return;
const fields = ['download_folder', 'refresh_interval', 'max_downloads', 'skip_pre_cache'];
const fields = ['download_folder', 'refresh_interval', 'max_downloads', 'skip_pre_cache', 'always_rm_tracker_urls'];
fields.forEach(field => {
const element = document.querySelector(`[name="qbit.${field}"]`);
@@ -1183,7 +1183,8 @@ class ConfigManager {
download_folder: document.querySelector('[name="qbit.download_folder"]').value,
refresh_interval: parseInt(document.querySelector('[name="qbit.refresh_interval"]').value) || 30,
max_downloads: parseInt(document.querySelector('[name="qbit.max_downloads"]').value) || 0,
skip_pre_cache: document.querySelector('[name="qbit.skip_pre_cache"]').checked
skip_pre_cache: document.querySelector('[name="qbit.skip_pre_cache"]').checked,
always_rm_tracker_urls: document.querySelector('[name="qbit.always_rm_tracker_urls"]').checked
};
}
+11
View File
@@ -9,6 +9,7 @@ class DownloadManager {
arr: document.getElementById('arr'),
downloadAction: document.getElementById('downloadAction'),
downloadUncached: document.getElementById('downloadUncached'),
rmTrackerUrls: document.getElementById('rmTrackerUrls'),
downloadFolder: document.getElementById('downloadFolder'),
debrid: document.getElementById('debrid'),
submitBtn: document.getElementById('submitDownload'),
@@ -34,6 +35,7 @@ class DownloadManager {
this.refs.arr.addEventListener('change', () => this.saveOptions());
this.refs.downloadAction.addEventListener('change', () => this.saveOptions());
this.refs.downloadUncached.addEventListener('change', () => this.saveOptions());
this.refs.rmTrackerUrls.addEventListener('change', () => this.saveOptions());
this.refs.downloadFolder.addEventListener('change', () => this.saveOptions());
// File input enhancement
@@ -48,12 +50,14 @@ class DownloadManager {
category: localStorage.getItem('downloadCategory') || '',
action: localStorage.getItem('downloadAction') || 'symlink',
uncached: localStorage.getItem('downloadUncached') === 'true',
rmTrackerUrls: localStorage.getItem('rmTrackerUrls') === 'true',
folder: localStorage.getItem('downloadFolder') || this.downloadFolder
};
this.refs.arr.value = savedOptions.category;
this.refs.downloadAction.value = savedOptions.action;
this.refs.downloadUncached.checked = savedOptions.uncached;
this.refs.rmTrackerUrls.checked = savedOptions.rmTrackerUrls;
this.refs.downloadFolder.value = savedOptions.folder;
}
@@ -61,6 +65,12 @@ class DownloadManager {
localStorage.setItem('downloadCategory', this.refs.arr.value);
localStorage.setItem('downloadAction', this.refs.downloadAction.value);
localStorage.setItem('downloadUncached', this.refs.downloadUncached.checked.toString());
// Only save rmTrackerUrls if not disabled (i.e., not forced by config)
if (!this.refs.rmTrackerUrls.disabled) {
localStorage.setItem('rmTrackerUrls', this.refs.rmTrackerUrls.checked.toString());
}
localStorage.setItem('downloadFolder', this.refs.downloadFolder.value);
}
@@ -114,6 +124,7 @@ class DownloadManager {
formData.append('downloadFolder', this.refs.downloadFolder.value);
formData.append('action', this.refs.downloadAction.value);
formData.append('downloadUncached', this.refs.downloadUncached.checked);
formData.append('rmTrackerUrls', this.refs.rmTrackerUrls.checked);
if (this.refs.debrid) {
formData.append('debrid', this.refs.debrid.value);
+10
View File
@@ -346,6 +346,16 @@
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" name="qbit.always_rm_tracker_urls" id="qbit.always_rm_tracker_urls">
<div>
<span class="label-text font-medium">Always Remove Tracker URLs</span>
<div class="label-text-alt">Allows you to <a href="https://sirrobot01.github.io/decypharr/features/repair-worker/private-tracker-downloads" class="link link-hover font-semibold" target="_blank">download private tracker torrents</a> with lower risk</div>
</div>
</label>
</div>
</div>
</div>
</div>
+9
View File
@@ -131,6 +131,15 @@
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" name="rmTrackerUrls" id="rmTrackerUrls" {{ if .AlwaysRmTrackerUrls }}checked disabled{{ end }}>
<div>
<span class="label-text font-medium">Remove Tracker</span>
<div class="label-text-alt">Allows you to <a href="https://sirrobot01.github.io/decypharr/features/repair-worker/private-tracker-downloads" class="link link-hover font-semibold" target="_blank">download private tracker torrents</a> with lower risk</div>
</div>
</label>
</div>
</div>
<div class="form-control">
+9 -8
View File
@@ -130,14 +130,15 @@ func (wb *Web) DownloadHandler(w http.ResponseWriter, r *http.Request) {
debrids = append(debrids, d.Name)
}
data := map[string]interface{}{
"URLBase": cfg.URLBase,
"Page": "download",
"Title": "Download",
"Debrids": debrids,
"HasMultiDebrid": len(debrids) > 1,
"DownloadFolder": cfg.QBitTorrent.DownloadFolder,
"NeedSetup": cfg.CheckSetup() != nil,
"SetupError": cfg.CheckSetup(),
"URLBase": cfg.URLBase,
"Page": "download",
"Title": "Download",
"Debrids": debrids,
"HasMultiDebrid": len(debrids) > 1,
"DownloadFolder": cfg.QBitTorrent.DownloadFolder,
"AlwaysRmTrackerUrls": cfg.QBitTorrent.AlwaysRmTrackerUrls,
"NeedSetup": cfg.CheckSetup() != nil,
"SetupError": cfg.CheckSetup(),
}
_ = wb.templates.ExecuteTemplate(w, "layout", data)
}