Skip to content

Commit 54b5119

Browse files
Merge pull request #161 from Vittuu/fix/allanime-goyabu-updater-2026-04
fix: stabilize download retries
2 parents ba8fb3c + 80ceaa5 commit 54b5119

4 files changed

Lines changed: 106 additions & 19 deletions

File tree

internal/player/download.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ import (
3232

3333
// Pre-compiled regexes for download quality parsing
3434
var (
35-
digitsRe = regexp.MustCompile(`\d+`)
36-
resolutionMp4Re = regexp.MustCompile(`(\d{3,4})p?\.mp4`)
35+
digitsRe = regexp.MustCompile(`\d+`)
36+
resolutionMp4Re = regexp.MustCompile(`(\d{3,4})p?\.mp4`)
37+
downloadPartRetryDelay = 500 * time.Millisecond
3738
)
3839

3940
// downloadPart downloads a part of the video file using HTTP Range Requests.
@@ -64,7 +65,7 @@ func downloadPart(url string, from, to int64, part int, client *http.Client, des
6465
}
6566
if attempt > 0 {
6667
util.Debugf("Download part %d: resuming at byte %d (attempt %d)", part, current, attempt+1)
67-
time.Sleep(500 * time.Millisecond)
68+
time.Sleep(downloadPartRetryDelay)
6869
}
6970

7071
beforeRead := current

internal/player/download_regression_test.go

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

33
import (
44
"bytes"
5+
"errors"
56
"io"
67
"net/http"
78
"net/http/httptest"
@@ -11,6 +12,7 @@ import (
1112
"strings"
1213
"sync"
1314
"testing"
15+
"time"
1416

1517
"charm.land/log/v2"
1618
"github.com/alvarorichard/Goanime/internal/models"
@@ -97,6 +99,74 @@ func TestDownloadPartAddsAllAnimeReferer(t *testing.T) {
9799
assert.Equal(t, payload, got)
98100
}
99101

102+
func TestDownloadPartStopsAfterRepeatedRequestErrors(t *testing.T) {
103+
restore := setDownloadPartRetryDelayForTest(0)
104+
defer restore()
105+
106+
var attempts int
107+
client := &http.Client{
108+
Transport: roundTripFunc(func(_ *http.Request) (*http.Response, error) {
109+
attempts++
110+
return nil, errors.New("temporary network failure")
111+
}),
112+
}
113+
114+
err := downloadPart(
115+
"https://allanime.day/video/episode.mp4",
116+
0,
117+
6,
118+
0,
119+
client,
120+
filepath.Join(t.TempDir(), "episode.mp4"),
121+
&model{},
122+
)
123+
124+
require.Error(t, err)
125+
assert.Contains(t, err.Error(), "max retries (20) exceeded")
126+
assert.Equal(t, 20, attempts)
127+
}
128+
129+
func TestDownloadPartStopsAfterRepeatedHTTPStatusWithoutProgress(t *testing.T) {
130+
restore := setDownloadPartRetryDelayForTest(0)
131+
defer restore()
132+
133+
var attempts int
134+
client := &http.Client{
135+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
136+
attempts++
137+
return &http.Response{
138+
StatusCode: http.StatusServiceUnavailable,
139+
Status: "503 Service Unavailable",
140+
Header: make(http.Header),
141+
Body: io.NopCloser(strings.NewReader("source unavailable")),
142+
Request: req,
143+
}, nil
144+
}),
145+
}
146+
147+
err := downloadPart(
148+
"https://allanime.day/video/episode.mp4",
149+
0,
150+
6,
151+
0,
152+
client,
153+
filepath.Join(t.TempDir(), "episode.mp4"),
154+
&model{},
155+
)
156+
157+
require.Error(t, err)
158+
assert.Contains(t, err.Error(), "max retries (20) exceeded")
159+
assert.Equal(t, 20, attempts)
160+
}
161+
162+
func setDownloadPartRetryDelayForTest(delay time.Duration) func() {
163+
original := downloadPartRetryDelay
164+
downloadPartRetryDelay = delay
165+
return func() {
166+
downloadPartRetryDelay = original
167+
}
168+
}
169+
100170
func TestGetContentLengthAddsAllAnimeReferer(t *testing.T) {
101171
const contentLength = "12345"
102172
var gotMethod string

internal/scraper/allanime.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -711,14 +711,19 @@ func (c *AllAnimeClient) processSourceURLsConcurrent(sourceURLs []string, qualit
711711

712712
// Collect results with timeout
713713
timeout := time.After(6 * time.Second)
714-
successCount := 0
714+
processedCount := 0
715715
var bestURL string
716716
var bestMetadata map[string]string
717+
var firstErr error
717718

718-
for successCount < len(sourceURLs) {
719+
for processedCount < len(sourceURLs) {
719720
select {
720721
case res := <-results:
722+
processedCount++
721723
if res.err != nil {
724+
if firstErr == nil {
725+
firstErr = res.err
726+
}
722727
continue
723728
}
724729

@@ -740,12 +745,14 @@ func (c *AllAnimeClient) processSourceURLsConcurrent(sourceURLs []string, qualit
740745
}
741746
}
742747
}
743-
successCount++
744748

745749
case <-timeout:
746750
if bestURL != "" {
747751
return bestURL, bestMetadata, nil
748752
}
753+
if firstErr != nil {
754+
return "", nil, fmt.Errorf("timeout waiting for results after %d/%d sources: %w", processedCount, len(sourceURLs), firstErr)
755+
}
749756
return "", nil, fmt.Errorf("timeout waiting for results")
750757
}
751758
}
@@ -754,6 +761,10 @@ func (c *AllAnimeClient) processSourceURLsConcurrent(sourceURLs []string, qualit
754761
return bestURL, bestMetadata, nil
755762
}
756763

764+
if firstErr != nil {
765+
return "", nil, fmt.Errorf("no suitable quality found from any source: %w", firstErr)
766+
}
767+
757768
return "", nil, fmt.Errorf("no suitable quality found from any source")
758769
}
759770

internal/scraper/allanime_test.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,11 +1478,14 @@ func TestProcessSourceURLsConcurrentAllFail(t *testing.T) {
14781478
defer failServer.Close()
14791479

14801480
client := newTestClient(failServer.URL)
1481+
startedAt := time.Now()
14811482
_, _, err := client.processSourceURLsConcurrent(
14821483
[]string{failServer.URL + "/1", failServer.URL + "/2"},
14831484
"best", "anime-id", "1",
14841485
)
1485-
assert.Error(t, err)
1486+
require.Error(t, err)
1487+
assert.Less(t, time.Since(startedAt), 2*time.Second, "failed sources should not wait for the global timeout")
1488+
assert.NotContains(t, err.Error(), "timeout waiting for results")
14861489
}
14871490

14881491
func TestProcessSourceURLsConcurrentPartialFailure(t *testing.T) {
@@ -1505,12 +1508,14 @@ func TestProcessSourceURLsConcurrentPartialFailure(t *testing.T) {
15051508
defer server.Close()
15061509

15071510
client := newTestClient(server.URL)
1511+
startedAt := time.Now()
15081512
url, _, err := client.processSourceURLsConcurrent(
15091513
[]string{server.URL + "/fail", server.URL + "/ok"},
15101514
"best", "anime-id", "1",
15111515
)
15121516
require.NoError(t, err)
15131517
assert.Equal(t, "https://cdn.example.com/720.mp4", url)
1518+
assert.Less(t, time.Since(startedAt), 2*time.Second, "successful fallback should not wait for the global timeout")
15141519
}
15151520

15161521
func TestProcessSourceURLsConcurrentHighPriorityWins(t *testing.T) {
@@ -1622,16 +1627,16 @@ func TestHTTPStatusCodes(t *testing.T) {
16221627
t.Parallel()
16231628

16241629
tests := []struct {
1625-
code int
1630+
code int
16261631
shouldBeUnavailable bool
16271632
}{
16281633
{200, false},
1629-
{301, true}, // redirect is non-2xx
1630-
{400, true}, // bad request
1631-
{403, true}, // forbidden -> ErrSourceUnavailable
1632-
{429, true}, // rate limited -> ErrSourceUnavailable
1633-
{500, true}, // internal server error
1634-
{503, true}, // service unavailable -> ErrSourceUnavailable
1634+
{301, true}, // redirect is non-2xx
1635+
{400, true}, // bad request
1636+
{403, true}, // forbidden -> ErrSourceUnavailable
1637+
{429, true}, // rate limited -> ErrSourceUnavailable
1638+
{500, true}, // internal server error
1639+
{503, true}, // service unavailable -> ErrSourceUnavailable
16351640
}
16361641

16371642
for _, tt := range tests {
@@ -1929,12 +1934,12 @@ func TestDecodeToBeParsedNoPanicOnMalformed(t *testing.T) {
19291934
t.Parallel()
19301935

19311936
inputs := []string{
1932-
"", // empty
1933-
"AA==", // 1 byte
1934-
"AAAAAAAAAAAAAAAA", // 12 bytes exactly (nonce only, too short)
1935-
"AAAAAAAAAAAAAAAAAAAA", // 15 bytes
1937+
"", // empty
1938+
"AA==", // 1 byte
1939+
"AAAAAAAAAAAAAAAA", // 12 bytes exactly (nonce only, too short)
1940+
"AAAAAAAAAAAAAAAAAAAA", // 15 bytes
19361941
base64.StdEncoding.EncodeToString(make([]byte, 100)), // 100 zero bytes
1937-
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz"
1942+
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=", // "abcdefghijklmnopqrstuvwxyz"
19381943
}
19391944

19401945
for i, input := range inputs {

0 commit comments

Comments
 (0)