Retry transient HLS network failures
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Useful environment variables:
|
|||||||
- `KAIZOKU_DOWNLOAD_DIR=/downloads` for the default library output path.
|
- `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_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_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.
|
- `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
|
## Docker
|
||||||
|
|||||||
@@ -420,6 +420,20 @@ def fetch_bytes_with_retries(url, headers=None, timeout=30, retries=None, retry_
|
|||||||
)
|
)
|
||||||
if delay:
|
if delay:
|
||||||
time.sleep(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():
|
def segment_download_delay_seconds():
|
||||||
|
|||||||
+33
@@ -4758,6 +4758,39 @@ https://shard-102.snapcdn.top/anime/show/episode/seg-f1-00083.css
|
|||||||
self.assertEqual(fetch_bytes.call_count, 2)
|
self.assertEqual(fetch_bytes.call_count, 2)
|
||||||
sleep.assert_called_once()
|
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):
|
def test_provider_stream_candidates_returns_resolved_sources(self):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
provider_downloader,
|
provider_downloader,
|
||||||
|
|||||||
Reference in New Issue
Block a user