Skip to content

Commit ba8fb3c

Browse files
Merge pull request #160 from Vittuu/fix/allanime-download-referer
fix(download): add missing Referer headers for AnimeFire and AllAnime CDN
2 parents 1da65da + d2a9c94 commit ba8fb3c

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

internal/player/blogger_extract_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package player
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/alvarorichard/Goanime/internal/util"
@@ -13,7 +14,43 @@ func TestExtractBloggerGoogleVideoURL(t *testing.T) {
1314

1415
videoURL, err := extractBloggerGoogleVideoURL(bloggerURL)
1516
if err != nil {
17+
if isTransientBloggerExtractError(err) {
18+
t.Skipf("Blogger source unavailable in test environment: %v", err)
19+
}
1620
t.Fatalf("extractBloggerGoogleVideoURL failed: %v", err)
1721
}
1822
t.Logf("Extracted video URL: %s", videoURL)
1923
}
24+
25+
func isTransientBloggerExtractError(err error) bool {
26+
if err == nil {
27+
return false
28+
}
29+
errMsg := strings.ToLower(err.Error())
30+
transient := []string{
31+
"no such host",
32+
"timeout",
33+
"temporary failure",
34+
"connection refused",
35+
"connection reset",
36+
"network is unreachable",
37+
"tls handshake timeout",
38+
"server returned",
39+
"status 403",
40+
"status 429",
41+
"status 500",
42+
"status 502",
43+
"status 503",
44+
"status 521",
45+
"status 522",
46+
"status 523",
47+
"status 524",
48+
"status 530",
49+
}
50+
for _, marker := range transient {
51+
if strings.Contains(errMsg, marker) {
52+
return true
53+
}
54+
}
55+
return false
56+
}

internal/player/download.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ func downloadPart(url string, from, to int64, part int, client *http.Client, des
7474
return err
7575
}
7676
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", current, to))
77+
if strings.Contains(url, "allanime.day") || strings.Contains(url, "allanime.pro") {
78+
req.Header.Set("Referer", "https://allanime.to")
79+
}
7780

7881
resp, err := client.Do(req) // #nosec G704
7982
if err != nil {
@@ -374,6 +377,11 @@ type directDownloadFunc func(string, string, *model) error
374377
type fallbackResolveFunc func(string, string) (string, error)
375378

376379
func downloadAnimeFireDirectWithFallback(videoAPIURL, videoURL, path string, m *model) error {
380+
// lightspeedst.net (AnimeFire CDN) requires Referer: https://animefire.io to authorise
381+
// token-signed requests. Ensure it is set before the download client sends any request.
382+
if util.GetGlobalReferer() == "" {
383+
util.SetGlobalReferer("https://animefire.io")
384+
}
377385
return runAnimeFireDirectDownloadWithFallback(
378386
videoAPIURL,
379387
videoURL,

internal/player/download_regression_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import (
1919
"github.com/stretchr/testify/require"
2020
)
2121

22+
type roundTripFunc func(*http.Request) (*http.Response, error)
23+
24+
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
25+
return fn(req)
26+
}
27+
2228
func TestDownloadDirectHTTPWithClientDownloadsMockVideoAndTracksProgress(t *testing.T) {
2329
payload := bytes.Repeat([]byte("goanime-video-payload"), 32*1024)
2430
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -52,6 +58,160 @@ func TestDownloadDirectHTTPWithClientDownloadsMockVideoAndTracksProgress(t *test
5258
assert.Equal(t, int64(len(payload)), received)
5359
}
5460

61+
func TestDownloadPartAddsAllAnimeReferer(t *testing.T) {
62+
payload := []byte("goanime")
63+
var gotReferer string
64+
var gotRange string
65+
66+
client := &http.Client{
67+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
68+
gotReferer = req.Header.Get("Referer")
69+
gotRange = req.Header.Get("Range")
70+
71+
return &http.Response{
72+
StatusCode: http.StatusPartialContent,
73+
Status: "206 Partial Content",
74+
Header: make(http.Header),
75+
Body: io.NopCloser(bytes.NewReader(payload)),
76+
Request: req,
77+
}, nil
78+
}),
79+
}
80+
81+
outPath := filepath.Join(t.TempDir(), "episode.mp4")
82+
err := downloadPart(
83+
"https://allanime.day/video/episode.mp4",
84+
0,
85+
int64(len(payload)-1),
86+
0,
87+
client,
88+
outPath,
89+
&model{},
90+
)
91+
require.NoError(t, err)
92+
assert.Equal(t, "https://allanime.to", gotReferer)
93+
assert.Equal(t, "bytes=0-6", gotRange)
94+
95+
got, err := os.ReadFile(outPath + ".part0")
96+
require.NoError(t, err)
97+
assert.Equal(t, payload, got)
98+
}
99+
100+
func TestGetContentLengthAddsAllAnimeReferer(t *testing.T) {
101+
const contentLength = "12345"
102+
var gotMethod string
103+
var gotReferer string
104+
105+
client := &http.Client{
106+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
107+
gotMethod = req.Method
108+
gotReferer = req.Header.Get("Referer")
109+
110+
header := make(http.Header)
111+
header.Set("Content-Length", contentLength)
112+
113+
return &http.Response{
114+
StatusCode: http.StatusOK,
115+
Status: "200 OK",
116+
Header: header,
117+
Body: io.NopCloser(strings.NewReader("")),
118+
Request: req,
119+
}, nil
120+
}),
121+
}
122+
123+
got, err := getContentLength("https://allanime.day/video/episode.mp4", client)
124+
require.NoError(t, err)
125+
assert.Equal(t, int64(12345), got)
126+
assert.Equal(t, http.MethodHead, gotMethod)
127+
assert.Equal(t, "https://allanime.to", gotReferer)
128+
}
129+
130+
func TestGetContentLengthFallbackKeepsAllAnimeReferer(t *testing.T) {
131+
var gotRequests []string
132+
var gotReferers []string
133+
var gotRanges []string
134+
135+
client := &http.Client{
136+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
137+
gotRequests = append(gotRequests, req.Method)
138+
gotReferers = append(gotReferers, req.Header.Get("Referer"))
139+
gotRanges = append(gotRanges, req.Header.Get("Range"))
140+
141+
if req.Method == http.MethodHead {
142+
return &http.Response{
143+
StatusCode: http.StatusMethodNotAllowed,
144+
Status: "405 Method Not Allowed",
145+
Header: make(http.Header),
146+
Body: io.NopCloser(strings.NewReader("")),
147+
Request: req,
148+
}, nil
149+
}
150+
151+
header := make(http.Header)
152+
header.Set("Content-Length", "1")
153+
154+
return &http.Response{
155+
StatusCode: http.StatusPartialContent,
156+
Status: "206 Partial Content",
157+
Header: header,
158+
Body: io.NopCloser(strings.NewReader("x")),
159+
Request: req,
160+
}, nil
161+
}),
162+
}
163+
164+
got, err := getContentLength("https://allanime.pro/video/episode.mp4", client)
165+
require.NoError(t, err)
166+
assert.Equal(t, int64(1), got)
167+
assert.Equal(t, []string{http.MethodHead, http.MethodGet}, gotRequests)
168+
assert.Equal(t, []string{"https://allanime.to", "https://allanime.to"}, gotReferers)
169+
assert.Equal(t, []string{"", "bytes=0-0"}, gotRanges)
170+
}
171+
172+
func TestDownloadAnimeFireDirectWithFallbackSetsDefaultReferer(t *testing.T) {
173+
restore := snapshotGlobalReferer()
174+
defer restore()
175+
util.ClearGlobalReferer()
176+
177+
err := downloadAnimeFireDirectWithFallback(
178+
"https://animefire.io/video/show/20",
179+
"://invalid-download-url",
180+
filepath.Join(t.TempDir(), "episode.mp4"),
181+
&model{},
182+
)
183+
184+
require.Error(t, err)
185+
assert.Equal(t, "https://animefire.io", util.GetGlobalReferer())
186+
}
187+
188+
func TestDownloadAnimeFireDirectWithFallbackKeepsExistingReferer(t *testing.T) {
189+
restore := snapshotGlobalReferer()
190+
defer restore()
191+
util.SetGlobalReferer("https://custom.example")
192+
193+
err := downloadAnimeFireDirectWithFallback(
194+
"https://animefire.io/video/show/20",
195+
"://invalid-download-url",
196+
filepath.Join(t.TempDir(), "episode.mp4"),
197+
&model{},
198+
)
199+
200+
require.Error(t, err)
201+
assert.Equal(t, "https://custom.example", util.GetGlobalReferer())
202+
}
203+
204+
func snapshotGlobalReferer() func() {
205+
referer := util.GetGlobalReferer()
206+
return func() {
207+
if referer == "" {
208+
util.ClearGlobalReferer()
209+
return
210+
}
211+
util.SetGlobalReferer(referer)
212+
}
213+
}
214+
55215
func TestDownloadDirectHTTPWithClientReturnsHTTPStatusErrorFromMockCDN(t *testing.T) {
56216
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
57217
http.Error(w, "missing object", http.StatusNotFound)

internal/player/scraper.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func getContentLength(url string, client *http.Client) (int64, error) {
8585
strings.Contains(url, "master.m3u8") ||
8686
strings.Contains(url, ".m3u8") ||
8787
strings.Contains(url, "allanime.pro") ||
88+
strings.Contains(url, "allanime.day") ||
8889
strings.Contains(url, "animefire") ||
8990
strings.Contains(url, "blogger.com") ||
9091
strings.Contains(url, "animesfire") ||
@@ -96,6 +97,9 @@ func getContentLength(url string, client *http.Client) (int64, error) {
9697
// Returns 0 and the error if the request creation fails.
9798
return 0, err
9899
}
100+
if strings.Contains(url, "allanime.day") || strings.Contains(url, "allanime.pro") {
101+
req.Header.Set("Referer", "https://allanime.to")
102+
}
99103

100104
// Sends the HEAD request to the server.
101105
resp, err := client.Do(req) // #nosec G704

0 commit comments

Comments
 (0)