Skip to content

Commit b72aa68

Browse files
s0up4200nuxencs
andauthored
fix: correct multi-tracker status logic for both down and unregistered states (#70)
* fix: correct multi-tracker status logic for both down and unregistered states Previously, torrents with multiple trackers could be incorrectly marked based on a single tracker's status. This fix ensures proper handling: - TrackerDown: Only when ALL trackers are down (no working trackers) - IsUnregistered: When ANY tracker reports the torrent as unregistered - Safety mechanism: Never mark as unregistered if all trackers are down - Maintains backward compatibility with single tracker status * fix: handle empty tracker messages as working trackers Empty tracker status messages typically indicate working trackers in qBittorrent. This fix ensures: - Empty status messages are counted as working trackers - AllTrackerStatuses includes all trackers (even with empty messages) - Torrents are only marked as TrackerDown when ALL trackers have error messages This resolves the issue where torrents with multiple trackers were incorrectly marked as down when only some trackers had issues. * fix: set RegistrationState when finding unregistered tracker Set t.RegistrationState = UnregisteredState when we find an unregistered tracker in the multi-tracker logic path for consistency and proper caching. * fix: simplify tracker down logic in IsTrackerDown method * refactor(torrent): simplify tracker down check * refactor: use testify assertions in test files Replace t.Errorf with assert methods from testify for cleaner and more consistent test assertions across the codebase. * fix(tracker): add intermediate state to torrent --------- Co-authored-by: nuxen <theonenuxen@proton.me>
1 parent 47206ca commit b72aa68

5 files changed

Lines changed: 788 additions & 8 deletions

File tree

pkg/client/deluge.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ func (c *Deluge) GetTorrents(ctx context.Context) (map[string]config.Torrent, er
192192
// tracker
193193
TrackerName: t.TrackerHost,
194194
TrackerStatus: t.TrackerStatus,
195+
// Note: Deluge only uses one tracker at a time, so AllTrackerStatuses is not populated
196+
AllTrackerStatuses: nil,
195197
}
196198

197199
torrents[h] = torrent

pkg/client/qbittorrent.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ func (c *QBittorrent) GetTorrents(ctx context.Context) (map[string]config.Torren
167167
// parse tracker details
168168
trackerName := ""
169169
trackerStatus := ""
170+
allTrackerStatuses := make(map[string]string)
170171

171172
var trackers []qbit.TorrentTracker
172173

@@ -181,17 +182,23 @@ func (c *QBittorrent) GetTorrents(ctx context.Context) (map[string]config.Torren
181182
trackers = ts
182183
}
183184

185+
firstTrackerSet := false
184186
for _, tr := range trackers {
185187
// skip disabled trackers
186188
if strings.Contains(tr.Url, "[DHT]") || strings.Contains(tr.Url, "[LSD]") ||
187189
strings.Contains(tr.Url, "[PeX]") {
188190
continue
189191
}
190192

191-
// use status of the first enabled tracker
192-
trackerName = config.ParseTrackerDomain(tr.Url)
193-
trackerStatus = tr.Message
194-
break
193+
// Store all tracker statuses
194+
allTrackerStatuses[tr.Url] = tr.Message
195+
196+
// Keep first tracker for backward compatibility
197+
if !firstTrackerSet {
198+
trackerName = config.ParseTrackerDomain(tr.Url)
199+
trackerStatus = tr.Message
200+
firstTrackerSet = true
201+
}
195202
}
196203

197204
// added time
@@ -249,9 +256,10 @@ func (c *QBittorrent) GetTorrents(ctx context.Context) (map[string]config.Torren
249256
FreeSpaceGB: c.GetFreeSpace,
250257
FreeSpaceSet: c.freeSpaceSet,
251258
// tracker
252-
TrackerName: trackerName,
253-
TrackerStatus: trackerStatus,
254-
Comment: td.Comment,
259+
TrackerName: trackerName,
260+
TrackerStatus: trackerStatus,
261+
AllTrackerStatuses: allTrackerStatuses,
262+
Comment: td.Comment,
255263
}
256264

257265
torrents[t.Hash] = torrent

pkg/client/qbittorrent_test.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package client
2+
3+
import (
4+
"testing"
5+
6+
"github.com/autobrr/go-qbittorrent"
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/autobrr/tqm/pkg/config"
10+
)
11+
12+
func TestQBittorrent_ProcessTrackerStatuses(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
trackers []qbittorrent.TorrentTracker
16+
expectedTrackerName string
17+
expectedTrackerStatus string
18+
expectedAllTrackerStatuses map[string]string
19+
}{
20+
{
21+
name: "multiple_trackers_first_down",
22+
trackers: []qbittorrent.TorrentTracker{
23+
{
24+
Url: "http://tracker1.com/announce",
25+
Message: "Connection failed",
26+
},
27+
{
28+
Url: "http://tracker2.com/announce",
29+
Message: "Working",
30+
},
31+
{
32+
Url: "http://tracker3.com/announce",
33+
Message: "Active",
34+
},
35+
},
36+
expectedTrackerName: "tracker1.com",
37+
expectedTrackerStatus: "Connection failed",
38+
expectedAllTrackerStatuses: map[string]string{
39+
"http://tracker1.com/announce": "Connection failed",
40+
"http://tracker2.com/announce": "Working",
41+
"http://tracker3.com/announce": "Active",
42+
},
43+
},
44+
{
45+
name: "skip_disabled_trackers",
46+
trackers: []qbittorrent.TorrentTracker{
47+
{
48+
Url: "[DHT]",
49+
Message: "DHT active",
50+
},
51+
{
52+
Url: "[LSD]",
53+
Message: "LSD active",
54+
},
55+
{
56+
Url: "[PeX]",
57+
Message: "PeX active",
58+
},
59+
{
60+
Url: "http://tracker1.com/announce",
61+
Message: "Working",
62+
},
63+
},
64+
expectedTrackerName: "tracker1.com",
65+
expectedTrackerStatus: "Working",
66+
expectedAllTrackerStatuses: map[string]string{
67+
"http://tracker1.com/announce": "Working",
68+
},
69+
},
70+
{
71+
name: "empty_tracker_messages",
72+
trackers: []qbittorrent.TorrentTracker{
73+
{
74+
Url: "http://tracker1.com/announce",
75+
Message: "",
76+
},
77+
{
78+
Url: "http://tracker2.com/announce",
79+
Message: "Working",
80+
},
81+
},
82+
expectedTrackerName: "tracker1.com",
83+
expectedTrackerStatus: "",
84+
expectedAllTrackerStatuses: map[string]string{
85+
"http://tracker2.com/announce": "Working",
86+
},
87+
},
88+
{
89+
name: "all_trackers_have_status",
90+
trackers: []qbittorrent.TorrentTracker{
91+
{
92+
Url: "http://tracker1.com/announce",
93+
Message: "timeout",
94+
},
95+
{
96+
Url: "http://tracker2.com/announce",
97+
Message: "connection refused",
98+
},
99+
{
100+
Url: "http://tracker3.com/announce",
101+
Message: "bad gateway",
102+
},
103+
},
104+
expectedTrackerName: "tracker1.com",
105+
expectedTrackerStatus: "timeout",
106+
expectedAllTrackerStatuses: map[string]string{
107+
"http://tracker1.com/announce": "timeout",
108+
"http://tracker2.com/announce": "connection refused",
109+
"http://tracker3.com/announce": "bad gateway",
110+
},
111+
},
112+
{
113+
name: "no_trackers",
114+
trackers: []qbittorrent.TorrentTracker{},
115+
expectedTrackerName: "",
116+
expectedTrackerStatus: "",
117+
expectedAllTrackerStatuses: map[string]string{},
118+
},
119+
{
120+
name: "tracker_url_with_port",
121+
trackers: []qbittorrent.TorrentTracker{
122+
{
123+
Url: "http://tracker1.com:8080/announce",
124+
Message: "Working",
125+
},
126+
},
127+
expectedTrackerName: "tracker1.com",
128+
expectedTrackerStatus: "Working",
129+
expectedAllTrackerStatuses: map[string]string{
130+
"http://tracker1.com:8080/announce": "Working",
131+
},
132+
},
133+
{
134+
name: "tracker_url_with_subdomain",
135+
trackers: []qbittorrent.TorrentTracker{
136+
{
137+
Url: "http://announce.tracker1.com/announce",
138+
Message: "Working",
139+
},
140+
},
141+
expectedTrackerName: "tracker1.com",
142+
expectedTrackerStatus: "Working",
143+
expectedAllTrackerStatuses: map[string]string{
144+
"http://announce.tracker1.com/announce": "Working",
145+
},
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
// Simulate the tracker processing logic from GetTorrents
152+
trackerName := ""
153+
trackerStatus := ""
154+
allTrackerStatuses := make(map[string]string)
155+
firstTrackerSet := false
156+
157+
for _, tr := range tt.trackers {
158+
// skip disabled trackers
159+
if tr.Url == "[DHT]" || tr.Url == "[LSD]" || tr.Url == "[PeX]" {
160+
continue
161+
}
162+
163+
// Store all tracker statuses
164+
if tr.Message != "" {
165+
allTrackerStatuses[tr.Url] = tr.Message
166+
}
167+
168+
// Keep first tracker for backward compatibility
169+
if !firstTrackerSet {
170+
trackerName = config.ParseTrackerDomain(tr.Url)
171+
trackerStatus = tr.Message
172+
firstTrackerSet = true
173+
}
174+
}
175+
176+
// Verify results
177+
assert.Equal(t, tt.expectedTrackerName, trackerName)
178+
assert.Equal(t, tt.expectedTrackerStatus, trackerStatus)
179+
assert.Len(t, allTrackerStatuses, len(tt.expectedAllTrackerStatuses))
180+
for url, status := range tt.expectedAllTrackerStatuses {
181+
assert.Equal(t, status, allTrackerStatuses[url])
182+
}
183+
})
184+
}
185+
}
186+
187+
func TestParseTrackerDomain(t *testing.T) {
188+
tests := []struct {
189+
name string
190+
trackerHost string
191+
expectedDomain string
192+
}{
193+
{
194+
name: "simple_url",
195+
trackerHost: "http://tracker.com/announce",
196+
expectedDomain: "tracker.com",
197+
},
198+
{
199+
name: "url_with_port",
200+
trackerHost: "http://tracker.com:8080/announce",
201+
expectedDomain: "tracker.com",
202+
},
203+
{
204+
name: "url_with_subdomain",
205+
trackerHost: "http://announce.tracker.com/announce",
206+
expectedDomain: "tracker.com",
207+
},
208+
{
209+
name: "https_url",
210+
trackerHost: "https://secure.tracker.com/announce",
211+
expectedDomain: "tracker.com",
212+
},
213+
{
214+
name: "complex_subdomain",
215+
trackerHost: "http://announce.sub.tracker.com/announce",
216+
expectedDomain: "tracker.com",
217+
},
218+
{
219+
name: "empty_host",
220+
trackerHost: "",
221+
expectedDomain: "",
222+
},
223+
{
224+
name: "invalid_url",
225+
trackerHost: "not-a-url",
226+
expectedDomain: "", // ParseTrackerDomain returns empty string for invalid URLs
227+
},
228+
{
229+
name: "ip_address",
230+
trackerHost: "http://192.168.1.1:8080/announce",
231+
expectedDomain: "192.168.1.1",
232+
},
233+
}
234+
235+
for _, tt := range tests {
236+
t.Run(tt.name, func(t *testing.T) {
237+
got := config.ParseTrackerDomain(tt.trackerHost)
238+
assert.Equal(t, tt.expectedDomain, got)
239+
})
240+
}
241+
}

0 commit comments

Comments
 (0)