fix(bot): notify chat when a queued job completes or fails

Without a push the failure looked like a hang (user had to check /queue
manually). _watch_job polls the job API (5s, ~15min cap) and sends
✅/❌ with the error text; title HTML-escaped.
This commit is contained in:
user1 committed 2026-10-13 21:45:36 +00:00
1 parent d6cceaa744
commit 0607ee488a
2 files changed
+61 -1

No files matched your search

+29 -1
View File
@@ -5,6 +5,8 @@ Simple functional Telegram bot for searching and downloading music.
No classes, no over-engineering - just functions.
"""
import asyncio
import html
import logging
import os
import sqlite3
@@ -799,6 +801,30 @@ async def callback_set_quality(update: Update, context: ContextTypes.DEFAULT_TYP
log_bot_action(chat_id, "settings", f"flac: {settings['convert_to_flac']}")
async def _watch_job(job_id: str, chat_id: int, message, title: str,
interval: float = 5.0, max_polls: int = 180):
"""Poll a queued job until it completes/fails, then notify the chat.
Fire-and-forget: without it the user never learns the outcome (looks
like a hang). ~15 min cap at 5s."""
status = "unknown"
for _ in range(max_polls):
await asyncio.sleep(interval)
data = await get_job_status(job_id)
if not data:
continue
status = data.get("status", "unknown")
if status in ("completed", "failed"):
break
safe_title = html.escape(title)
if status == "completed":
await message.chat.send_message(f"✅ <b>Загружено</b>\n\n🎵 {safe_title}", parse_mode="HTML")
elif status == "failed":
err = html.escape((data or {}).get("error") or "неизвестная ошибка")
await message.chat.send_message(f"❌ <b>Ошибка загрузки</b>\n\n🎵 {safe_title}\nПричина: {err}", parse_mode="HTML")
else:
logger.info(f"job {job_id}: watcher gave up after {max_polls} polls (last: {status})")
async def callback_download(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle download callback."""
query = update.callback_query
@@ -855,7 +881,9 @@ async def callback_download(update: Update, context: ContextTypes.DEFAULT_TYPE):
🎵 {track.get('title', 'Unknown')}
🎤 {track.get('artist') or track.get('channel', 'Unknown')}
Используйте /queue для отслеживания прогресса."""
Придёт уведомление о результате (или /queue для прогресса)."""
asyncio.create_task(_watch_job(job_id, chat_id, query.message, track.get('title', 'Unknown')))
await query.edit_message_text(text, parse_mode="HTML")
log_bot_action(chat_id, "download", f"{track.get('title')}, job: {job_id}")
+32
View File
@@ -66,6 +66,38 @@ def main():
assert app.concurrent_updates, "concurrent_updates выключен: медленный хендлер блокирует остальные кнопки"
# _watch_job уведомляет чат об итоге (иначе провал выглядит как зависание)
import asyncio
class FakeChat:
def __init__(self):
self.sent = []
async def send_message(self, text, **kw):
self.sent.append(text)
class FakeMessage:
def __init__(self):
self.chat = FakeChat()
async def run_case(status_json, expect):
msg = FakeMessage()
tb.get_job_status = lambda job_id: _async_ret(status_json)
await tb._watch_job("j", 1, msg, "T<i>tle", interval=0, max_polls=2)
if expect is None:
assert not msg.chat.sent, f"не должно быть отправок, а было: {msg.chat.sent}"
else:
assert msg.chat.sent and expect in msg.chat.sent[0], f"ожидалось '{expect}', отправлено: {msg.chat.sent}"
assert "<i>" not in msg.chat.sent[0], "HTML в title не экранирован"
async def _async_ret(v):
return v
asyncio.run(run_case({"status": "completed"}, "✅"))
asyncio.run(run_case({"status": "failed", "error": "DRM"}, "❌"))
asyncio.run(run_case({"status": "downloading"}, None)) # не завершился — молча
print("OK: watcher шлёт ✅/❌ по итогу задания")
print(f"OK: все {len(all_callback_datas())} видов callback_data покрыты хендлерами, concurrent_updates включён")