Skip to content

Commit c3c8d66

Browse files
authored
fix(crossseed): compare Site and Sum fields for anime releases (#769)
1 parent fafd278 commit c3c8d66

2 files changed

Lines changed: 307 additions & 0 deletions

File tree

internal/services/crossseed/matching.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,27 @@ func (s *Service) releasesMatch(source, candidate *rls.Release, findIndividualEp
205205
}
206206
// If source has no group, we don't care about candidate's group
207207

208+
// Site field is used by anime releases where group is in brackets like [SubsPlease].
209+
// rls parses these as Site rather than Group. Different fansub groups can never
210+
// cross-seed, so enforce strict matching like Group.
211+
sourceSite := s.stringNormalizer.Normalize(source.Site)
212+
candidateSite := s.stringNormalizer.Normalize(candidate.Site)
213+
if sourceSite != "" {
214+
if candidateSite == "" || sourceSite != candidateSite {
215+
return false
216+
}
217+
}
218+
219+
// Sum field contains the CRC32 checksum for anime releases like [32ECE75A].
220+
// Different checksums mean different files with 100% certainty.
221+
sourceSum := s.stringNormalizer.Normalize(source.Sum)
222+
candidateSum := s.stringNormalizer.Normalize(candidate.Sum)
223+
if sourceSum != "" {
224+
if candidateSum == "" || sourceSum != candidateSum {
225+
return false
226+
}
227+
}
228+
208229
// Source must match if both are present (WEB-DL vs BluRay produce different files)
209230
sourceSource := s.stringNormalizer.Normalize((source.Source))
210231
candidateSource := s.stringNormalizer.Normalize((candidate.Source))
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package crossseed
2+
3+
import (
4+
"testing"
5+
6+
"github.com/moistari/rls"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/autobrr/qui/pkg/stringutils"
10+
)
11+
12+
func TestReleasesMatch_SiteMustMatch(t *testing.T) {
13+
s := &Service{stringNormalizer: stringutils.NewDefaultNormalizer()}
14+
15+
tests := []struct {
16+
name string
17+
source rls.Release
18+
candidate rls.Release
19+
wantMatch bool
20+
description string
21+
}{
22+
{
23+
name: "different sites should not match",
24+
source: rls.Release{
25+
Title: "Kingdom",
26+
Series: 6,
27+
Episode: 11,
28+
Site: "SubsPlease",
29+
},
30+
candidate: rls.Release{
31+
Title: "Kingdom",
32+
Series: 6,
33+
Episode: 11,
34+
Site: "AnoZu",
35+
},
36+
wantMatch: false,
37+
description: "different fansub groups (Site field) should NOT match",
38+
},
39+
{
40+
name: "same sites should match",
41+
source: rls.Release{
42+
Title: "Kingdom",
43+
Series: 6,
44+
Episode: 11,
45+
Site: "SubsPlease",
46+
},
47+
candidate: rls.Release{
48+
Title: "Kingdom",
49+
Series: 6,
50+
Episode: 11,
51+
Site: "SubsPlease",
52+
},
53+
wantMatch: true,
54+
description: "same fansub group should match",
55+
},
56+
{
57+
name: "source has site, candidate does not - should not match",
58+
source: rls.Release{
59+
Title: "Kingdom",
60+
Series: 6,
61+
Episode: 11,
62+
Site: "SubsPlease",
63+
},
64+
candidate: rls.Release{
65+
Title: "Kingdom",
66+
Series: 6,
67+
Episode: 11,
68+
},
69+
wantMatch: false,
70+
description: "candidate must have matching site when source has one",
71+
},
72+
{
73+
name: "candidate has site, source does not - should match",
74+
source: rls.Release{
75+
Title: "Kingdom",
76+
Series: 6,
77+
Episode: 11,
78+
},
79+
candidate: rls.Release{
80+
Title: "Kingdom",
81+
Series: 6,
82+
Episode: 11,
83+
Site: "SubsPlease",
84+
},
85+
wantMatch: true,
86+
description: "source has no site so site matching is not enforced",
87+
},
88+
{
89+
name: "site comparison is case insensitive",
90+
source: rls.Release{
91+
Title: "Show",
92+
Series: 1,
93+
Episode: 1,
94+
Site: "SUBSPLEASE",
95+
},
96+
candidate: rls.Release{
97+
Title: "Show",
98+
Series: 1,
99+
Episode: 1,
100+
Site: "subsplease",
101+
},
102+
wantMatch: true,
103+
description: "site comparison should be case insensitive",
104+
},
105+
}
106+
107+
for _, tt := range tests {
108+
t.Run(tt.name, func(t *testing.T) {
109+
result := s.releasesMatch(&tt.source, &tt.candidate, false)
110+
if tt.wantMatch {
111+
require.True(t, result, tt.description)
112+
} else {
113+
require.False(t, result, tt.description)
114+
}
115+
})
116+
}
117+
}
118+
119+
func TestReleasesMatch_SumMustMatch(t *testing.T) {
120+
s := &Service{stringNormalizer: stringutils.NewDefaultNormalizer()}
121+
122+
tests := []struct {
123+
name string
124+
source rls.Release
125+
candidate rls.Release
126+
wantMatch bool
127+
description string
128+
}{
129+
{
130+
name: "different checksums should not match",
131+
source: rls.Release{
132+
Title: "Kingdom",
133+
Series: 6,
134+
Episode: 11,
135+
Site: "SubsPlease",
136+
Sum: "32ECE75A",
137+
},
138+
candidate: rls.Release{
139+
Title: "Kingdom",
140+
Series: 6,
141+
Episode: 11,
142+
Site: "SubsPlease",
143+
Sum: "DEADBEEF",
144+
},
145+
wantMatch: false,
146+
description: "different CRC32 checksums indicate different encodes",
147+
},
148+
{
149+
name: "same checksums should match",
150+
source: rls.Release{
151+
Title: "Kingdom",
152+
Series: 6,
153+
Episode: 11,
154+
Site: "SubsPlease",
155+
Sum: "32ECE75A",
156+
},
157+
candidate: rls.Release{
158+
Title: "Kingdom",
159+
Series: 6,
160+
Episode: 11,
161+
Site: "SubsPlease",
162+
Sum: "32ECE75A",
163+
},
164+
wantMatch: true,
165+
description: "same checksum should match",
166+
},
167+
{
168+
name: "source has sum, candidate does not - should not match",
169+
source: rls.Release{
170+
Title: "Kingdom",
171+
Series: 6,
172+
Episode: 11,
173+
Site: "SubsPlease",
174+
Sum: "32ECE75A",
175+
},
176+
candidate: rls.Release{
177+
Title: "Kingdom",
178+
Series: 6,
179+
Episode: 11,
180+
Site: "SubsPlease",
181+
},
182+
wantMatch: false,
183+
description: "candidate must have matching checksum when source has one",
184+
},
185+
{
186+
name: "candidate has sum, source does not - should match",
187+
source: rls.Release{
188+
Title: "Kingdom",
189+
Series: 6,
190+
Episode: 11,
191+
Site: "SubsPlease",
192+
},
193+
candidate: rls.Release{
194+
Title: "Kingdom",
195+
Series: 6,
196+
Episode: 11,
197+
Site: "SubsPlease",
198+
Sum: "32ECE75A",
199+
},
200+
wantMatch: true,
201+
description: "source has no checksum so checksum matching is not enforced",
202+
},
203+
{
204+
name: "sum comparison is case insensitive",
205+
source: rls.Release{
206+
Title: "Show",
207+
Series: 1,
208+
Episode: 1,
209+
Sum: "ABCD1234",
210+
},
211+
candidate: rls.Release{
212+
Title: "Show",
213+
Series: 1,
214+
Episode: 1,
215+
Sum: "abcd1234",
216+
},
217+
wantMatch: true,
218+
description: "checksum comparison should be case insensitive",
219+
},
220+
}
221+
222+
for _, tt := range tests {
223+
t.Run(tt.name, func(t *testing.T) {
224+
result := s.releasesMatch(&tt.source, &tt.candidate, false)
225+
if tt.wantMatch {
226+
require.True(t, result, tt.description)
227+
} else {
228+
require.False(t, result, tt.description)
229+
}
230+
})
231+
}
232+
}
233+
234+
func TestReleasesMatch_AnimeRealWorld(t *testing.T) {
235+
s := &Service{stringNormalizer: stringutils.NewDefaultNormalizer()}
236+
237+
// These are parsed by rls from real torrent names
238+
subsPlease := rls.Release{
239+
Title: "Kingdom",
240+
Series: 6,
241+
Episode: 11,
242+
Resolution: "1080p",
243+
Site: "SubsPlease",
244+
Sum: "32ECE75A",
245+
}
246+
247+
subsPleaseMatchingSum := rls.Release{
248+
Title: "Kingdom",
249+
Series: 6,
250+
Episode: 11,
251+
Resolution: "1080p",
252+
Site: "SubsPlease",
253+
Sum: "32ECE75A",
254+
}
255+
256+
anoZu := rls.Release{
257+
Title: "Kingdom",
258+
Series: 6,
259+
Episode: 11,
260+
Resolution: "1080p",
261+
Source: "WEB-DL",
262+
Codec: []string{"H264"},
263+
Site: "AnoZu",
264+
}
265+
266+
// Source without checksum can match candidate with or without checksum
267+
subsPleaseNoSum := rls.Release{
268+
Title: "Kingdom",
269+
Series: 6,
270+
Episode: 11,
271+
Resolution: "1080p",
272+
Site: "SubsPlease",
273+
}
274+
275+
require.False(t, s.releasesMatch(&subsPlease, &anoZu, false),
276+
"SubsPlease and AnoZu are different fansub groups - should NOT match")
277+
278+
require.True(t, s.releasesMatch(&subsPlease, &subsPleaseMatchingSum, false),
279+
"same SubsPlease release with matching checksum should match")
280+
281+
require.False(t, s.releasesMatch(&subsPlease, &subsPleaseNoSum, false),
282+
"source has checksum so candidate must have matching checksum")
283+
284+
require.True(t, s.releasesMatch(&subsPleaseNoSum, &subsPlease, false),
285+
"source without checksum can match candidate with checksum")
286+
}

0 commit comments

Comments
 (0)