Skip to content

Commit fa015d8

Browse files
committed
Let one job be claimed by one torrent
Matching on the infohash fixes the repack, but the title fallback is still a substring test in both directions, so two torrents in the category whose names both contain a job's title each merged with that same job -- and the endpoint emitted the one job as two cards, which is the duplicate this is meant to remove. The first torrent to match takes the job; a later one is listed as the bare torrent it is, rather than dropped.
1 parent 3ce5a79 commit fa015d8

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

internal/api/api.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,13 @@ func (s *Server) handleDownloads(w http.ResponseWriter, r *http.Request) {
694694
// were both emitted and the UI drew two cards for one download.
695695
found := false
696696
for _, item := range jobs.Items() {
697+
// One job belongs to one torrent. A job with no infohash falls back to
698+
// a substring match on the title, which two torrents in the category
699+
// can both satisfy, and merging it twice draws the same job as two
700+
// cards - the duplicate this endpoint is meant to stop.
701+
if matchedJobIDs[item.ID] {
702+
continue
703+
}
697704
jobTitle, _ := item.Data["title"].(string)
698705
jobHash, _ := item.Data["info_hash"].(string)
699706
if download.JobMatchesTorrent(jobHash, jobTitle, t.Hash, t.Name) {

internal/api/handlers_crud_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,52 @@ func TestDownloadsMergesTorrentWithJobByHash(t *testing.T) {
284284
}
285285
}
286286

287+
func TestDownloadsClaimsAJobOnlyOnce(t *testing.T) {
288+
// A job recorded before the infohash was stored matches on the title, and
289+
// titlesMatch is a substring test either way -- so a second torrent whose
290+
// name contains the same title matches the very same job.
291+
const jobName = "Hollow Knight Silksong"
292+
293+
qb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
294+
if strings.HasSuffix(r.URL.Path, "/auth/login") {
295+
w.Write([]byte("Ok."))
296+
return
297+
}
298+
json.NewEncoder(w).Encode([]map[string]interface{}{
299+
{"name": jobName + " [FitGirl Repack]", "hash": "aaaa", "progress": 0.4,
300+
"state": "downloading", "total_size": 100, "dlspeed": 1, "eta": 60},
301+
{"name": jobName + " (Update v1.1)", "hash": "bbbb", "progress": 0.8,
302+
"state": "downloading", "total_size": 100, "dlspeed": 1, "eta": 60},
303+
})
304+
}))
305+
defer qb.Close()
306+
307+
env := newTestEnv(t, func(c *config.Config) { c.QBURL = qb.URL })
308+
env.jobs.Set("job-silksong", map[string]interface{}{
309+
"status": "downloading", "title": jobName,
310+
})
311+
312+
rr := env.do("GET", "/api/downloads", "")
313+
wantStatus(t, rr, 200)
314+
downloads, _ := decodeMap(t, rr)["downloads"].([]interface{})
315+
316+
seen := map[string]int{}
317+
for _, d := range downloads {
318+
entry, _ := d.(map[string]interface{})
319+
if id, _ := entry["job_id"].(string); id != "" {
320+
seen[id]++
321+
}
322+
}
323+
if seen["job-silksong"] != 1 {
324+
t.Errorf("job-silksong appears in %d entries, want 1", seen["job-silksong"])
325+
}
326+
// The torrent that lost the claim is still a real download and has to be
327+
// listed -- dropping it would trade a duplicate for a disappearance.
328+
if len(downloads) != 2 {
329+
t.Errorf("downloads = %d entries, want 2 (one merged job, one bare torrent)", len(downloads))
330+
}
331+
}
332+
287333
func TestDownloadsListClearAndDelete(t *testing.T) {
288334
env := newTestEnv(t, nil)
289335

0 commit comments

Comments
 (0)