diff --git a/backend/app/main.py b/backend/app/main.py index 692718b..13fb347 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -21,9 +21,6 @@ from .telegram import start_telegram_client, stop_telegram_client from .routers import files_router, folders_router, streaming_router, auth_router, tv_router -# Import bot to register handlers -from . import bot # noqa - settings = get_settings() # Rate limiter - uses IP address by default diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 9c4b13c..e7c89cd 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -27,7 +27,7 @@ verify_token_payload, get_current_user, ) -from ..telegram import tg_client +from .. import telegram # Get limiter from main app limiter = Limiter(key_func=get_remote_address) @@ -40,7 +40,7 @@ async def get_bot_info_endpoint(): """Get bot username and name for the login screen.""" try: - me = await tg_client.get_me() + me = await telegram.tg_client.get_me() return BotInfoResponse( username=me.username, name=f"{me.first_name} {me.last_name or ''}".strip(), diff --git a/backend/app/routers/streaming.py b/backend/app/routers/streaming.py index d5f57c4..365b3d0 100644 --- a/backend/app/routers/streaming.py +++ b/backend/app/routers/streaming.py @@ -13,7 +13,8 @@ from ..database import get_db from ..models import File, User from ..auth import get_current_user -from ..telegram import get_message_from_channel, tg_client +from .. import telegram +from ..telegram import get_message_from_channel from ..streaming import stream_file as stream_file_generator # Logger for internal debugging (not exposed to users) @@ -82,7 +83,7 @@ async def stream_file( async def file_streamer(): """Generator that streams file chunks from Telegram MTProto.""" async for chunk in stream_file_generator( - tg_client, + telegram.tg_client, message, from_bytes, until_bytes @@ -149,14 +150,14 @@ async def get_thumbnail( # Try using the file_id directly if stored (fallback) if file.thumbnail_file_id: try: - thumb_bytes = await tg_client.download_media(file.thumbnail_file_id, in_memory=True) + thumb_bytes = await telegram.tg_client.download_media(file.thumbnail_file_id, in_memory=True) return Response(content=thumb_bytes.getvalue(), media_type="image/jpeg") except Exception: pass raise HTTPException(status_code=404, detail="Thumbnail not found in message") # Download thumbnail to memory - thumb_bytes = await tg_client.download_media(thumbnail.file_id, in_memory=True) + thumb_bytes = await telegram.tg_client.download_media(thumbnail.file_id, in_memory=True) return Response( content=thumb_bytes.getvalue(), @@ -208,7 +209,7 @@ async def stream_public_file( async def file_streamer(): """Generator that streams file chunks from Telegram MTProto.""" async for chunk in stream_file_generator( - tg_client, + telegram.tg_client, message, from_bytes, until_bytes diff --git a/backend/app/telegram.py b/backend/app/telegram.py index d616f22..e4ee23f 100644 --- a/backend/app/telegram.py +++ b/backend/app/telegram.py @@ -21,25 +21,30 @@ def get_session_name(index: int) -> str: return str(SESSION_DIR / f"bot_{index}") -# Build a pool: main token first, then any helper tokens -tokens = settings.all_bot_tokens -clients = [] - -for i, token in enumerate(tokens): - client = Client( - name=get_session_name(i), - api_id=settings.telegram_api_id, - api_hash=settings.telegram_api_hash, - bot_token=token, - ipv6=False, - max_concurrent_transmissions=settings.telegram_client_concurrency, - no_updates=(i > 0), # only main client receives updates - ) - client.pool_index = i # custom attr for logging - clients.append(client) - -# Main client — used for bot commands, message fetching, forwarding, etc. -tg_client = clients[0] +# Created in build_clients() inside the running event loop (see start_telegram_client). +clients: list[Client] = [] +tg_client: Client | None = None + + +def build_clients() -> None: + """Build client pool lazily so Pyrogram binds to uvicorn's event loop.""" + global tg_client + if clients: + return + SESSION_DIR.mkdir(parents=True, exist_ok=True) + for i, token in enumerate(settings.all_bot_tokens): + client = Client( + name=get_session_name(i), + api_id=settings.telegram_api_id, + api_hash=settings.telegram_api_hash, + bot_token=token, + ipv6=False, + max_concurrent_transmissions=settings.telegram_client_concurrency, + no_updates=(i > 0), # only main client receives updates + ) + client.pool_index = i # custom attr for logging + clients.append(client) + tg_client = clients[0] # ── lifecycle helpers ──────────────────────────────────────────────── @@ -75,6 +80,8 @@ async def stop_all_clients(): async def start_telegram_client(): """Called from app lifespan — starts the full pool.""" + build_clients() + from . import bot # noqa: F401 — register handlers after client exists await start_all_clients() diff --git a/docker-compose.yml b/docker-compose.yml index 9cd6a72..3f1f035 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,8 +65,8 @@ services: ports: - "${BACKEND_PORT:-8000}:8000" volumes: - # Persist Telegram session - - telegram_session:/app + # Persist Telegram session files only (do not mount over /app — that hides rebuilt code) + - telegram_session:/app/session networks: - telegram-tv-network