Fix duplicate-post and timeout bugs in TG/MAX posting pipeline

- Stop blindly retrying send_photo/send_video/send_media_group and MAX
  send_message on ambiguous network timeouts - a timeout doesn't prove
  the message wasn't delivered, and retrying risked posting duplicates
  (observed live: a video posted 3-5x after repeated timeout retries).
- Raise/rework timeouts that were too short for real large-file transfer
  speeds: TG media upload timeout, and yt-dlp download now uses stall
  detection (killed only on true silence) instead of a flat ceiling that
  was cutting off legitimately slow-but-successful video downloads.
- Fix sendRichMessage: ok=true with an unparseable message_id no longer
  triggers a fallback send (Telegram already created the message).
- MAX: videos over the documented 250MB cap are now sent as a "watch via
  link" note instead of silently failing the upload.
- Add PRAGMA busy_timeout to all DB connections.
- Remove unused MAX_MEDIA_CHANNEL_ID (MAX's /uploads returns a portable
  token directly, no staging channel needed, unlike Telegram).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 12:17:47 +05:00
parent be01558ddb
commit ec65aaf57d
9 changed files with 276 additions and 108 deletions
+34 -16
View File
@@ -132,36 +132,54 @@ class MediaProcessor:
f"/bestvideo[height<={settings.video_max_height}][filesize<{max_size}]+bestaudio"
f"/bestvideo[height<={settings.video_max_height}]+bestaudio"
),
"--quiet", "--no-warnings",
# No --quiet: we need yt-dlp's own progress lines to tell a slow-but-alive
# download (fine, however long it takes) apart from a genuinely hung one -
# a flat wall-clock timeout can't tell those apart and was killing large
# videos that just needed more time (same class of bug as the TG upload
# timeout). --newline makes each progress update its own line instead of
# overwriting via \r, so we can read it with readline().
"--newline", "--no-warnings",
])
proc = None
stderr = b""
output_lines: list[str] = []
deadline = asyncio.get_running_loop().time() + settings.yt_dlp_timeout_sec
stall_reason: Optional[str] = None
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
_, stderr = await asyncio.wait_for(
proc.communicate(), timeout=settings.yt_dlp_timeout_sec
)
except asyncio.TimeoutError:
if proc:
while True:
remaining_total = deadline - asyncio.get_running_loop().time()
if remaining_total <= 0:
stall_reason = "video download exceeded overall timeout"
break
wait_for = min(settings.yt_dlp_stall_timeout_sec, remaining_total)
try:
proc.kill()
await proc.communicate()
except Exception:
pass
output_path.unlink(missing_ok=True)
await unregister_active_path(output_path)
return None, "video download timeout", False
line = await asyncio.wait_for(proc.stdout.readline(), timeout=wait_for)
except asyncio.TimeoutError:
stall_reason = "video download stalled (no progress from yt-dlp)"
break
if not line:
break # stdout closed - process is finishing up
output_lines.append(line.decode("utf-8", errors="ignore"))
if stall_reason:
proc.kill()
await proc.communicate()
output_path.unlink(missing_ok=True)
await unregister_active_path(output_path)
return None, stall_reason, False
await proc.wait()
finally:
if netrc_path:
Path(netrc_path).unlink(missing_ok=True)
if proc.returncode != 0 or not output_path.exists() or output_path.stat().st_size == 0:
err_msg = (stderr or b"").decode("utf-8", errors="ignore").lower()
err_msg = "".join(output_lines).lower()
is_permanent = any(
m in err_msg for m in (
"removed", "unavailable", "private", "access denied", "does not pass filter", "sign in"