Skip to content

Commit 39f17db

Browse files
committed
feat: refactor source breakdown logic for case-insensitive matching and add corresponding tests
1 parent c37ed4c commit 39f17db

4 files changed

Lines changed: 146 additions & 29 deletions

File tree

internal/api/enhanced.go

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -146,30 +146,16 @@ func SearchAnimeEnhanced(name string, source string) (*models.Anime, error) {
146146

147147
util.Debug("Search results summary", "total", len(animes))
148148

149-
// Show sources breakdown in debug only
150-
animefireCount := 0
151-
allanimeCount := 0
152-
animedriveCount := 0
153-
flixhqCount := 0
154-
nineAnimeCount := 0
155-
superflixCount := 0
156-
for _, anime := range animes {
157-
if strings.Contains(anime.Source, "AnimeFire") {
158-
animefireCount++
159-
} else if anime.Source == "AllAnime" {
160-
allanimeCount++
161-
} else if anime.Source == "AnimeDrive" {
162-
animedriveCount++
163-
} else if anime.Source == "FlixHQ" {
164-
flixhqCount++
165-
} else if anime.Source == "9Anime" {
166-
nineAnimeCount++
167-
} else if anime.Source == "SuperFlix" {
168-
superflixCount++
169-
}
170-
}
171-
172-
util.Debug("Source breakdown", "AnimeFire", animefireCount, "AllAnime", allanimeCount, "AnimeDrive", animedriveCount, "FlixHQ", flixhqCount, "9Anime", nineAnimeCount, "SuperFlix", superflixCount)
149+
breakdown := countSourceBreakdown(animes)
150+
util.Debug("Source breakdown",
151+
"AnimeFire", breakdown.AnimeFire,
152+
"AllAnime", breakdown.AllAnime,
153+
"AnimeDrive", breakdown.AnimeDrive,
154+
"FlixHQ", breakdown.FlixHQ,
155+
"9Anime", breakdown.NineAnime,
156+
"SuperFlix", breakdown.SuperFlix,
157+
"Goyabu", breakdown.Goyabu,
158+
)
173159

174160
// Sort results by language priority: Portuguese first, then Multilanguage, Movies/TV, English, others
175161
sort.SliceStable(animes, func(i, j int) bool {
@@ -279,10 +265,12 @@ func GetAnimeEpisodesEnhanced(anime *models.Anime) ([]models.Episode, error) {
279265
// Determine source type from multiple indicators with enhanced logic
280266
var sourceName string
281267

282-
// Priority 1: Check the Source field (most reliable)
268+
// Priority 1: Check the Source field (most reliable). Use a case-insensitive
269+
// match for AnimeFire because the scraper emits "Animefire.io" (lowercase 'f')
270+
// while older code paths/tests sometimes use the camelcase spelling "AnimeFire".
283271
if anime.Source == "AllAnime" {
284272
sourceName = "AllAnime"
285-
} else if strings.Contains(anime.Source, "AnimeFire") {
273+
} else if strings.Contains(strings.ToLower(anime.Source), "animefire") {
286274
sourceName = "Animefire.io"
287275
} else if anime.Source == "AnimeDrive" {
288276
sourceName = "AnimeDrive"
@@ -1151,6 +1139,49 @@ func GetSuperFlixStreamURL(media *models.Anime, episode *models.Episode, quality
11511139
return result.StreamURL, nil
11521140
}
11531141

1142+
// sourceBreakdown holds per-source result counts for the debug "Source breakdown"
1143+
// diagnostic line. Counted via countSourceBreakdown so the predicate stays
1144+
// testable in isolation.
1145+
type sourceBreakdown struct {
1146+
AnimeFire int
1147+
AllAnime int
1148+
AnimeDrive int
1149+
FlixHQ int
1150+
NineAnime int
1151+
SuperFlix int
1152+
Goyabu int
1153+
}
1154+
1155+
// countSourceBreakdown tallies anime results by Source field using
1156+
// case-insensitive matching for AnimeFire. The scraper canonical Source is
1157+
// "Animefire.io" (lowercase 'f'), but older callers and tests sometimes emit
1158+
// "AnimeFire"; both must be counted so the diagnostic line never lies.
1159+
func countSourceBreakdown(animes []*models.Anime) sourceBreakdown {
1160+
var b sourceBreakdown
1161+
for _, anime := range animes {
1162+
if anime == nil {
1163+
continue
1164+
}
1165+
switch {
1166+
case strings.Contains(strings.ToLower(anime.Source), "animefire"):
1167+
b.AnimeFire++
1168+
case anime.Source == "AllAnime":
1169+
b.AllAnime++
1170+
case anime.Source == "AnimeDrive":
1171+
b.AnimeDrive++
1172+
case anime.Source == "FlixHQ":
1173+
b.FlixHQ++
1174+
case anime.Source == "9Anime":
1175+
b.NineAnime++
1176+
case anime.Source == "SuperFlix":
1177+
b.SuperFlix++
1178+
case anime.Source == "Goyabu":
1179+
b.Goyabu++
1180+
}
1181+
}
1182+
return b
1183+
}
1184+
11541185
// languagePriority returns a sort key for language-based ordering.
11551186
// Lower values sort first: Portuguese → Multilanguage → English → Movies/TV → Unknown.
11561187
func languagePriority(name string) int {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package api
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/alvarorichard/Goanime/internal/models"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// TestSourceBreakdown_LegacyCaseSensitiveBug documents the original case-sensitive
12+
// regression: scrapers emit anime.Source = "Animefire.io" (lowercase 'f'), but the
13+
// debug breakdown searched for the substring "AnimeFire" (capital 'F'). Because
14+
// strings.Contains is byte-exact in Go, the AnimeFire counter was always zero
15+
// even when the search returned dozens of AnimeFire results — making the
16+
// "Source breakdown" diagnostic line lie to operators.
17+
//
18+
// This test reproduces the buggy comparison in isolation. The expectation
19+
// matches the broken behaviour so that if anyone ever "fixes" the bug by
20+
// hand without updating both the production code AND this regression test,
21+
// the failure makes the intent obvious.
22+
func TestSourceBreakdown_LegacyCaseSensitiveBug(t *testing.T) {
23+
source := "Animefire.io"
24+
25+
// The exact pre-fix predicate from internal/api/enhanced.go.
26+
matchesLegacyPredicate := strings.Contains(source, "AnimeFire")
27+
28+
assert.False(t, matchesLegacyPredicate,
29+
"sanity check: 'Animefire.io' must NOT match 'AnimeFire' under case-sensitive Contains — "+
30+
"if this assertion changes, Go's strings package semantics changed and the rest of the suite needs review")
31+
}
32+
33+
// TestCountSourceBreakdown_AnimeFireCaseInsensitive is the positive regression
34+
// test for the fix: the breakdown helper must count "Animefire.io" results
35+
// regardless of capitalisation. Future scrapers (or upstream renames) that
36+
// emit "AnimeFire", "ANIMEFIRE", or "animefire" must all be counted.
37+
func TestCountSourceBreakdown_AnimeFireCaseInsensitive(t *testing.T) {
38+
animes := []*models.Anime{
39+
{Source: "Animefire.io"},
40+
{Source: "Animefire.io"},
41+
{Source: "AnimeFire"},
42+
{Source: "ANIMEFIRE"},
43+
{Source: "animefire"},
44+
{Source: "AllAnime"},
45+
{Source: "Goyabu"},
46+
{Source: "FlixHQ"},
47+
{Source: "9Anime"},
48+
{Source: "SuperFlix"},
49+
{Source: "AnimeDrive"},
50+
}
51+
52+
got := countSourceBreakdown(animes)
53+
54+
assert.Equal(t, 5, got.AnimeFire, "all AnimeFire spellings must be counted")
55+
assert.Equal(t, 1, got.AllAnime)
56+
assert.Equal(t, 1, got.Goyabu)
57+
assert.Equal(t, 1, got.FlixHQ)
58+
assert.Equal(t, 1, got.NineAnime)
59+
assert.Equal(t, 1, got.SuperFlix)
60+
assert.Equal(t, 1, got.AnimeDrive)
61+
}
62+
63+
// TestCountSourceBreakdown_RealisticPayload mirrors the user-reported log:
64+
// 10 AnimeFire results, 5 AllAnime, 8 Goyabu — and asserts that Goyabu is
65+
// reported (the original breakdown silently dropped it).
66+
func TestCountSourceBreakdown_RealisticPayload(t *testing.T) {
67+
var animes []*models.Anime
68+
for i := 0; i < 10; i++ {
69+
animes = append(animes, &models.Anime{Source: "Animefire.io"})
70+
}
71+
for i := 0; i < 5; i++ {
72+
animes = append(animes, &models.Anime{Source: "AllAnime"})
73+
}
74+
for i := 0; i < 8; i++ {
75+
animes = append(animes, &models.Anime{Source: "Goyabu"})
76+
}
77+
78+
got := countSourceBreakdown(animes)
79+
80+
assert.Equal(t, 10, got.AnimeFire, "AnimeFire breakdown must equal what the scraper returned")
81+
assert.Equal(t, 5, got.AllAnime)
82+
assert.Equal(t, 8, got.Goyabu, "Goyabu must appear in the breakdown")
83+
assert.Equal(t, 0, got.AnimeDrive)
84+
assert.Equal(t, 0, got.FlixHQ)
85+
assert.Equal(t, 0, got.NineAnime)
86+
assert.Equal(t, 0, got.SuperFlix)
87+
}

internal/playback/handle_download_and_play_call_site_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
// downstream, and would silently restore the original failure mode.
2222
//
2323
// This test parses the two call sites in the playback package and asserts:
24-
// 1. positional index 5 (the 6th argument) reads field MalID, and
25-
// 2. positional index 6 (the 7th argument) reads field AnilistID.
24+
// 1. positional index 5 (the 6th argument) reads field MalID, and
25+
// 2. positional index 6 (the 7th argument) reads field AnilistID.
2626
//
2727
// If either invariant breaks, the test fails with a precise message.
2828
func TestHandleDownloadAndPlay_CallSites_PassDistinctIDFields(t *testing.T) {

internal/player/animefire_referer_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,4 +422,3 @@ func TestApplyDownloadAuthHeaders_NilRequestIsNoop(t *testing.T) {
422422
applyDownloadAuthHeaders(nil, "https://lightspeedst.net/x")
423423
})
424424
}
425-

0 commit comments

Comments
 (0)