Retry transient video download failures, friendlier fallback link text, enable previews

- yt-dlp: retry transient failures (stall/timeout) up to 2x before falling
  back to a link-only post; permanent errors (private/removed) still fail fast
- Fallback note: friendlier wording, "Смотреть видео" as link text instead of
  a bare "ссылка", plus a link to the original VK wall post as a backup
- TG: enable link preview for text-only fallback posts (was unconditionally
  disabled, leaving fallback posts with no visual at all)
- MAX: attach the VK video's own thumbnail as an image so failed/oversized
  video fallbacks still show a preview picture (MAX has no OG-preview for
  arbitrary URLs, but does accept a remote image url without upload)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 20:36:22 +05:00
parent 3a086d9e54
commit 4ad047b5c3
6 changed files with 64 additions and 7 deletions
+23
View File
@@ -49,6 +49,7 @@ class ProcessedMedia:
height: Optional[int] = None
error: Optional[str] = None
is_link_only: bool = False
thumbnail_url: Optional[str] = None
class MediaProcessor:
@@ -100,6 +101,26 @@ 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."""
last_result: tuple[Optional[Path], Optional[str], bool] = (None, "video download failed", False)
for attempt in range(1, settings.yt_dlp_retry_attempts + 2):
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:
logger.warning(
"yt-dlp attempt {}/{} failed transiently for {}: {} - retrying",
attempt, settings.yt_dlp_retry_attempts + 1, url, err,
)
await asyncio.sleep(3.0)
return last_result
async def _download_video_ytdlp_attempt(
self, url: str, prefix: str = "video_"
) -> tuple[Optional[Path], Optional[str], bool]:
"""Returns (path, error_message, is_permanent_error)"""
output_path = self.cache_dir / f"{prefix}{uuid.uuid4().hex[:12]}.mp4"
@@ -249,6 +270,7 @@ class MediaProcessor:
duration_sec=item.duration_sec,
error="video duration exceeds maximum",
is_link_only=True,
thumbnail_url=item.thumbnail_url,
)
)
continue
@@ -277,6 +299,7 @@ class MediaProcessor:
duration_sec=item.duration_sec,
error=err or "failed to download video",
is_link_only=True,
thumbnail_url=item.thumbnail_url,
)
)
return results