From 0e1f25384e825f6153cffe7db10ac6807f573e81 Mon Sep 17 00:00:00 2001 From: exostring Date: Sat, 5 Sep 2026 00:13:19 +0500 Subject: [PATCH] Give oversized-video retries a much longer budget than stall retries Confirmed on a real post: VK can still be transcoding lower renditions 8+ minutes after publish, so a few seconds of backoff wasn't enough margin. Size-exceeded failures now get their own retry budget (5 attempts, 90s apart - up to 7.5 extra minutes) instead of sharing the short 3s backoff meant for stalls/timeouts. Co-Authored-By: Claude Sonnet 5 --- src/config.py | 6 ++++++ src/media_processor.py | 33 ++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/config.py b/src/config.py index 7dcd68c..8f5ccf7 100644 --- a/src/config.py +++ b/src/config.py @@ -83,6 +83,12 @@ class Settings(BaseSettings): # Extra attempts after a transient failure (stall/timeout/network hiccup) before # falling back to a link-only post. Permanent failures (private/removed/etc.) never retry. yt_dlp_retry_attempts: int = 2 + # A fresh upload's lower-quality renditions can still be mid-transcode on VK's side, + # so a height-capped URL briefly serves the full-res master and trips our size check - + # confirmed on a real post still happening 8 minutes after publish. Needs a separate, + # much longer retry budget than the generic stall/timeout case above. + yt_dlp_size_retry_attempts: int = 5 + yt_dlp_size_retry_delay_sec: float = 90.0 # Text Styling & Decoration header_text: str = "" diff --git a/src/media_processor.py b/src/media_processor.py index 09fcfc1..d293627 100644 --- a/src/media_processor.py +++ b/src/media_processor.py @@ -102,24 +102,35 @@ class MediaProcessor: async def download_video_ytdlp( self, url: str, prefix: str = "video_" ) -> tuple[Optional[Path], Optional[str], bool]: - """Retries transient failures (stalls, timeouts, one-off yt-dlp/network hiccups) - a couple of times before giving up - a single flaky attempt used to fall straight - through to the link-only fallback even though the video was perfectly downloadable.""" + """Retries transient failures before giving up - a single flaky attempt used to fall + straight through to the link-only fallback even though the video was perfectly + downloadable. Stalls/timeouts/network hiccups get a couple of quick retries; a + size-exceeded result (likely VK still transcoding lower renditions) switches to a + much longer, more patient retry budget - see yt_dlp_size_retry_* in config.py.""" last_result: tuple[Optional[Path], Optional[str], bool] = (None, "video download failed", False) - for attempt in range(1, settings.yt_dlp_retry_attempts + 2): + attempt = 0 + max_attempts = settings.yt_dlp_retry_attempts + 1 + delay = 3.0 + while attempt < max_attempts: + attempt += 1 path, err, is_permanent = await self._download_video_ytdlp_attempt(url, prefix) if path or is_permanent: return path, err, is_permanent last_result = (path, err, is_permanent) - if attempt <= settings.yt_dlp_retry_attempts: - # A fresh upload's lower-quality renditions can still be mid-transcode on - # VK's side - the "720p" URL then briefly serves the full-res master until - # the ladder catches up, which trips our size cap. That's not a permanent - # rejection, just needs more time than a stalled connection does. - delay = 60.0 if "exceeds size limit" in (err or "") else 3.0 + # A fresh upload's lower-quality renditions can still be mid-transcode on + # VK's side - the "720p" URL then briefly serves the full-res master until + # the ladder catches up, which trips our size cap. Confirmed on a real post + # still happening 8 minutes after publish, so this needs a much longer and + # more patient retry budget than a stalled connection does - switch to it + # (and restart the attempt count) the first time we see this specific error. + if "exceeds size limit" in (err or "") and max_attempts != settings.yt_dlp_size_retry_attempts + 1: + max_attempts = settings.yt_dlp_size_retry_attempts + 1 + delay = settings.yt_dlp_size_retry_delay_sec + attempt = 0 + if attempt < max_attempts: logger.warning( "yt-dlp attempt {}/{} failed transiently for {}: {} - retrying in {}s", - attempt, settings.yt_dlp_retry_attempts + 1, url, err, delay, + attempt, max_attempts, url, err, delay, ) await asyncio.sleep(delay) return last_result