Add curl fallback for protected HLS segments
This commit is contained in:
+69
-2
@@ -10,6 +10,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
@@ -19,6 +20,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
BRIDGE = PROJECT_ROOT / "providers" / "bridge.js"
|
||||
PROVIDERS = ("anikoto", "anineko", "pahe")
|
||||
RETRYABLE_HTTP_STATUS = {408, 425, 429, 500, 502, 503, 504}
|
||||
CURL_STATUS_MARKER = b"\nKAIZOKU_HTTP_STATUS:"
|
||||
|
||||
|
||||
def clean_component(value, default="Anime"):
|
||||
@@ -212,10 +214,75 @@ def request_headers(headers=None, referer=None):
|
||||
return merged
|
||||
|
||||
|
||||
def curl_candidates():
|
||||
configured = str(os.environ.get("KAIZOKU_CURL_BIN") or "").strip()
|
||||
names = [
|
||||
configured,
|
||||
"curl_chrome120",
|
||||
"curl_chrome116",
|
||||
"curl_chrome110",
|
||||
"curl-impersonate",
|
||||
"curl",
|
||||
]
|
||||
candidates = []
|
||||
for name in names:
|
||||
if not name or name in candidates:
|
||||
continue
|
||||
resolved = shutil.which(name)
|
||||
if resolved:
|
||||
candidates.append(resolved)
|
||||
return candidates
|
||||
|
||||
|
||||
def curl_fetch_bytes(url, headers=None, timeout=30):
|
||||
candidates = curl_candidates()
|
||||
if not candidates:
|
||||
raise RuntimeError("curl is not available for media fetch fallback.")
|
||||
clean_headers = request_headers(headers)
|
||||
last_error = None
|
||||
for curl_bin in candidates:
|
||||
cmd = [
|
||||
curl_bin,
|
||||
"--location",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--max-time",
|
||||
str(max(1, int(timeout or 30))),
|
||||
]
|
||||
for key, value in clean_headers.items():
|
||||
if value:
|
||||
cmd.extend(["--header", f"{key}: {value}"])
|
||||
cmd.extend(["--write-out", f"{CURL_STATUS_MARKER.decode('ascii')}%{{http_code}}", "--output", "-", str(url)])
|
||||
proc = subprocess.run(cmd, capture_output=True, check=False)
|
||||
payload, marker, status_bytes = proc.stdout.rpartition(CURL_STATUS_MARKER)
|
||||
if marker:
|
||||
try:
|
||||
status = int(status_bytes.strip() or b"0")
|
||||
except ValueError:
|
||||
status = 0
|
||||
else:
|
||||
payload = proc.stdout
|
||||
status = 0
|
||||
if 200 <= status < 300 and proc.returncode == 0:
|
||||
return payload
|
||||
message = (proc.stderr or b"curl media fetch failed").decode("utf-8", errors="replace").strip()
|
||||
last_error = urllib.error.HTTPError(str(url), status or 0, message, hdrs={}, fp=None)
|
||||
raise last_error or RuntimeError("curl media fetch failed.")
|
||||
|
||||
|
||||
def fetch_bytes(url, headers=None, timeout=30):
|
||||
client = str(os.environ.get("KAIZOKU_MEDIA_HTTP_CLIENT") or "auto").strip().lower()
|
||||
if client == "curl":
|
||||
return curl_fetch_bytes(url, headers=headers, timeout=timeout)
|
||||
request = urllib.request.Request(url, headers=request_headers(headers))
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
return response.read()
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
return response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
if client != "urllib" and exc.code == 403 and curl_candidates():
|
||||
print(f"urllib media fetch got HTTP 403; retrying with curl fallback.", file=sys.stderr, flush=True)
|
||||
return curl_fetch_bytes(url, headers=headers, timeout=timeout)
|
||||
raise
|
||||
|
||||
|
||||
def int_env(name, default, minimum=0):
|
||||
|
||||
Reference in New Issue
Block a user