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