diff --git a/backend/app/downloads/formats.py b/backend/app/downloads/formats.py index cd5d766..8fc710d 100644 --- a/backend/app/downloads/formats.py +++ b/backend/app/downloads/formats.py @@ -91,10 +91,17 @@ def build_ydl_opts(spec: dict, outtmpl: str, progress_hook) -> dict: "writethumbnail": True, # kept for the poster.jpg sidecar (and embed, if requested) "progress_hooks": [progress_hook], "postprocessors": [], + # Force IPv4: containers often have no working IPv6 route, but googlevideo CDN hosts + # advertise AAAA records, so ffmpeg/the downloader would try IPv6 and fail with + # "[Errno -5] No address associated with hostname". Binding to the IPv4 any-address + # makes every connection IPv4-only. (yt-dlp's --force-ipv4 does the same thing.) + "source_address": "0.0.0.0", # Self-heal transient network blips instead of failing the whole job. - "retries": 3, - "fragment_retries": 5, - "retry_sleep": {"http": 2, "fragment": 2}, + "retries": 10, + "fragment_retries": 10, + "extractor_retries": 3, + "retry_sleep": {"http": 5, "fragment": 5, "extractor": 5}, + "socket_timeout": 30, } if s["mode"] == "a": diff --git a/backend/app/routes/downloads.py b/backend/app/routes/downloads.py index a5e3358..661f0ae 100644 --- a/backend/app/routes/downloads.py +++ b/backend/app/routes/downloads.py @@ -364,6 +364,14 @@ def resume_download( if job.status in ("paused", "error"): job.progress = 0 job.error = None + # If the shared asset itself failed, reset it to pending so the worker actually + # re-downloads — otherwise the requeued job would instantly re-inherit the asset's + # stale error (the worker short-circuits a job whose asset is already errored). + if job.asset_id: + asset = db.get(MediaAsset, job.asset_id) + if asset and asset.status == "error": + asset.status = "pending" + asset.error = None _set_status(db, job, "queued") asset = db.get(MediaAsset, job.asset_id) if job.asset_id else None return _serialize(job, asset)