Retry transient HLS network failures

This commit is contained in:
Dymas
2026-09-07 23:09:00 +02:00
parent a5401873cb
commit 91228d58e0
5 changed files with 53 additions and 2 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## 0.52.29 - 2026-09-07
- Added retry and exponential backoff for temporary HLS segment network failures, including DNS resolution, connection, and socket timeout errors.
## 0.52.28 - 2026-09-07
- Fixed provider source fallback so playlist-resolution exceptions such as `HTTP 502` fail only the current server and the downloader continues with the remaining same-mode servers.
+1 -1
View File
@@ -67,7 +67,7 @@ Useful environment variables:
- `KAIZOKU_DOWNLOAD_DIR=/downloads` for the default library output path.
- `KAIZOKU_REMOTE_PATH_ROOTS=/downloads,/jellyfin/tv,/jellyfin/movies` to limit remote filesystem browsing; comma-separated and platform path separators are accepted.
- `KAIZOKU_MODE=sub`, `KAIZOKU_QUALITY=best`, and `KAIZOKU_DEBUG=1` for runtime defaults and diagnostics.
- `KAIZOKU_SEGMENT_RETRIES=8`, `KAIZOKU_SEGMENT_RETRY_DELAY=1.25`, `KAIZOKU_SEGMENT_RETRY_MAX_DELAY=15`, and `KAIZOKU_SEGMENT_DOWNLOAD_DELAY=0` to tune retry/backoff and optional pacing behavior for native HLS segment downloads.
- `KAIZOKU_SEGMENT_RETRIES=8`, `KAIZOKU_SEGMENT_RETRY_DELAY=1.25`, `KAIZOKU_SEGMENT_RETRY_MAX_DELAY=15`, and `KAIZOKU_SEGMENT_DOWNLOAD_DELAY=0` to tune retry/backoff and optional pacing behavior for native HLS segment downloads. Retries cover temporary HTTP responses, DNS failures, connection errors, and socket timeouts.
- `KAIZOKU_MEDIA_HTTP_CLIENT=auto` to let protected HLS segment fetches fall back from Python HTTP to `curl` or curl-impersonate after `HTTP 403`; use `curl` or `urllib` to force one client, and `KAIZOKU_CURL_BIN=/path/to/curl_chrome142` to prefer a specific curl-compatible binary. Playlist, segment, and encryption-key requests preserve provider CDN session cookies for the duration of the download process.
## Docker
+1 -1
View File
@@ -1 +1 @@
0.52.28
0.52.29
+14
View File
@@ -420,6 +420,20 @@ def fetch_bytes_with_retries(url, headers=None, timeout=30, retries=None, retry_
)
if delay:
time.sleep(delay)
except (urllib.error.URLError, TimeoutError, ConnectionError) as exc:
if attempt >= attempts:
raise
delay = base_delay * (2 ** attempt)
if max_delay:
delay = min(delay, max_delay)
print(
f"{retry_label} had a temporary network failure; retrying in {delay:.1f}s "
f"({attempt + 1}/{attempts}): {exc}",
file=sys.stderr,
flush=True,
)
if delay:
time.sleep(delay)
def segment_download_delay_seconds():
+33
View File
@@ -4758,6 +4758,39 @@ https://shard-102.snapcdn.top/anime/show/episode/seg-f1-00083.css
self.assertEqual(fetch_bytes.call_count, 2)
sleep.assert_called_once()
def test_fetch_bytes_with_retries_recovers_from_temporary_dns_failure(self):
error = urllib.error.URLError(OSError(-3, "Temporary failure in name resolution"))
with mock.patch.object(
provider_downloader,
"fetch_bytes",
side_effect=[error, b"video"],
) as fetch_bytes, mock.patch.object(provider_downloader.time, "sleep") as sleep:
data = provider_downloader.fetch_bytes_with_retries(
"https://cdn.example/video/raw-segment",
retries=1,
retry_label="segment",
)
self.assertEqual(data, b"video")
self.assertEqual(fetch_bytes.call_count, 2)
sleep.assert_called_once()
def test_fetch_bytes_with_retries_raises_dns_failure_after_limit(self):
error = urllib.error.URLError(OSError(-3, "Temporary failure in name resolution"))
with mock.patch.object(provider_downloader, "fetch_bytes", side_effect=error), mock.patch.object(
provider_downloader.time, "sleep"
) as sleep:
with self.assertRaises(urllib.error.URLError):
provider_downloader.fetch_bytes_with_retries(
"https://cdn.example/video/raw-segment",
retries=1,
retry_label="segment",
)
sleep.assert_called_once()
def test_provider_stream_candidates_returns_resolved_sources(self):
with mock.patch.object(
provider_downloader,