From 4b15d550928f20d2271110c6d113213afc8f3f1e Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 3 Jul 2026 03:47:13 +0200 Subject: [PATCH] fix(downloads): force IPv4 (fixes container 'Errno -5' DNS failures) + retry resets errored asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the failing downloads: containers usually have no working IPv6 route, but googlevideo CDN hosts advertise AAAA records, so the downloader tried IPv6 and failed with '[Errno -5] No address associated with hostname'. Small clips happened to use IPv4; long videos (more CDN requests / the ffmpeg path) hit IPv6 and died. - yt-dlp source_address=0.0.0.0 forces IPv4 for every connection (== --force-ipv4). Verified: the full video+audio download completes cleanly. - Also: retry (resume) now resets the shared asset from 'error' to 'pending', so it actually re-downloads — previously the requeued job instantly re-inherited the asset's stale error because the worker short-circuits a job whose asset already errored. - Dropped the localdev DNS override (wrong hypothesis; the issue was IPv6, not the resolver). - Kept yt-dlp retries/fragment_retries/socket_timeout for transient blips. Verified end-to-end in the worker: two previously-failing long videos now download to done. --- backend/app/downloads/formats.py | 13 ++++++++++--- backend/app/routes/downloads.py | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) 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)