From a36afe3aa2ce75366e2f8b0225f319bf13f1fb8b Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Mon, 1 Sep 2025 08:24:36 +0530 Subject: [PATCH 1/7] feat: implement health check server with Docker support --- Dockerfile | 5 +++ TgMusic/__init__.py | 73 +++------------------------------------- TgMusic/core/__init__.py | 2 ++ TgMusic/core/_config.py | 1 + TgMusic/core/_health.py | 58 +++++++++++++++++++++++++++++++ TgMusic/core/_tgcalls.py | 2 +- 6 files changed, 72 insertions(+), 69 deletions(-) create mode 100644 TgMusic/core/_health.py diff --git a/Dockerfile b/Dockerfile index 4121052e..d4c5fc3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ + wget \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv @@ -12,4 +13,8 @@ COPY . /app/ RUN uv pip install -e . --system +# Health check configuration +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:5068/health || exit 1 + CMD ["tgmusic"] diff --git a/TgMusic/__init__.py b/TgMusic/__init__.py index 062ae318..20671ecc 100644 --- a/TgMusic/__init__.py +++ b/TgMusic/__init__.py @@ -13,7 +13,7 @@ StartTime = datetime.now() -from TgMusic.core import call, tg, db, config +from TgMusic.core import call, tg, db, config, HealthCheck class Bot(Client): @@ -32,7 +32,7 @@ def __init__(self) -> None: database_encryption_key="", options={"ignore_background_updates": config.IGNORE_BACKGROUND_UPDATES}, ) - + self.health_check = HealthCheck(client=self, port=config.PORT) self._initialize_services() def _initialize_services(self) -> None: @@ -80,9 +80,10 @@ async def _initialize_components(self) -> None: await self.call.register_decorators() await super().start() await self.call_manager.start() + await self.health_check.start() self.logger.info("Bot started successfully") - self.loop.create_task(self.watch_dog()) + async def stop(self, graceful: bool = True) -> None: self.logger.info("Stopping bot...") @@ -91,6 +92,7 @@ async def stop(self, graceful: bool = True) -> None: self.db.close(), self.call_manager.stop(), self.call.stop_all_clients(), + self.health_check.stop(), ] if graceful: @@ -105,69 +107,4 @@ def _get_uptime(self) -> float: """Calculate bot uptime in seconds.""" return (datetime.now() - self._start_time).total_seconds() - async def watch_dog(self): - consecutive_failures = 0 - max_backoff = 300 - while True: - try: - if not self.is_running: - self.logger.warning("Bot not running, attempting restart...") - await self._restart() - - try: - await self.call.health_check() - consecutive_failures = 0 - await asyncio.sleep(300) - continue - except Exception as e: - self.logger.error(f"Health check failed: {e}", exc_info=True) - consecutive_failures += 1 - - backoff = min(5 * (2 ** consecutive_failures), max_backoff) - backoff = backoff * (0.5 + random.random()) - - self.logger.warning( - f"Health check failed {consecutive_failures} times. " - f"Retrying in {backoff:.1f}s..." - ) - - if consecutive_failures >= 3: - await self._restart() - - await asyncio.sleep(backoff) - - except asyncio.CancelledError: - self.logger.info("Watchdog stopped by cancellation") - raise - except Exception as e: - self.logger.critical( - f"Critical error in watchdog: {e}", - exc_info=True - ) - await asyncio.sleep(100) - - async def _restart(self): - import traceback - - try: - self.logger.info("Initiating safe restart...") - await self.stop(graceful=True) - await asyncio.sleep(2) - if hasattr(self, 'call') and hasattr(self.call, 'pyrogram_clients'): - for _client in self.call.pyrogram_clients.values(): - try: - if _client.is_connected: - await client.stop() - except Exception as e: - self.logger.error(f"Error stopping client: {e}") - await self.start() - self.logger.info("Restart completed successfully") - - except Exception as e: - self.logger.critical( - f"Failed to restart: {e}\n{traceback.format_exc()}" - ) - raise - - client: Client = Bot() diff --git a/TgMusic/core/__init__.py b/TgMusic/core/__init__.py index 1db023d8..c6c50a02 100644 --- a/TgMusic/core/__init__.py +++ b/TgMusic/core/__init__.py @@ -21,6 +21,7 @@ from ._filters import Filter from .buttons import SupportButton, control_buttons from ._save_cookies import save_all_cookies +from ._health import HealthCheck __all__ = [ "admins_only", @@ -46,4 +47,5 @@ "PlatformTracks", "SupportButton", "Filter", + "HealthCheck" ] diff --git a/TgMusic/core/_config.py b/TgMusic/core/_config.py index 3cb36e19..1a38dbc9 100644 --- a/TgMusic/core/_config.py +++ b/TgMusic/core/_config.py @@ -39,6 +39,7 @@ def __init__(self): self.LOGGER_ID: int = self._get_env_int("LOGGER_ID", -1002166934878) # Optional Settings + self.PORT: int = self._get_env_int("PORT", 5068) self.PROXY: Optional[str] = os.getenv("PROXY") self.DEFAULT_SERVICE: str = os.getenv("DEFAULT_SERVICE", "youtube").lower() self.MIN_MEMBER_COUNT: int = self._get_env_int("MIN_MEMBER_COUNT", 50) diff --git a/TgMusic/core/_health.py b/TgMusic/core/_health.py new file mode 100644 index 00000000..ec89050b --- /dev/null +++ b/TgMusic/core/_health.py @@ -0,0 +1,58 @@ +from aiohttp import web +import asyncio + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from TgMusic import Bot + +class HealthCheck: + def __init__(self, client: 'Bot', port, host='0.0.0.0'): + self.client = client + self.port = port + self.host = host + self.app = web.Application() + self.runner = None + self.site = None + + async def home(self, _: web.Request): + bot_name = self.client.me.first_name + return web.json_response({ + 'Bot': bot_name, + 'version': getattr(self.client, '_version', 'unknown'), + 'uptime': getattr(self.client, '_get_uptime', lambda: 0)() + }) + + async def health_check(self, _: web.Request): + if not self.client: + raise web.HTTPServiceUnavailable(text="Client not initialized") + + if not getattr(self.client, 'is_running', False): + raise web.HTTPServiceUnavailable(text="Client not running") + + try: + await self.client.call.health_check() + except RuntimeError as e: + self.client.logger.error(f"Health check failed: {e}") + raise web.HTTPServiceUnavailable(text="Pyrogram client not running") + + return web.json_response({ + 'status': 'healthy', + 'version': getattr(self.client, '_version', 'unknown'), + 'uptime': getattr(self.client, '_get_uptime', lambda: 0)(), + 'timestamp': asyncio.get_event_loop().time(), + }) + + async def start(self): + self.app.router.add_get('/', self.home) + self.app.router.add_get('/health', self.health_check) + self.runner = web.AppRunner(self.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, self.host, self.port) + await self.site.start() + self.client.logger.info(f"Health check server started on http://{self.host}:{self.port}") + + async def stop(self): + if self.site: + await self.site.stop() + if self.runner: + await self.runner.cleanup() diff --git a/TgMusic/core/_tgcalls.py b/TgMusic/core/_tgcalls.py index 440f8532..64289dbd 100644 --- a/TgMusic/core/_tgcalls.py +++ b/TgMusic/core/_tgcalls.py @@ -152,7 +152,7 @@ async def health_check(self) -> None: for name, client in self.pyrogram_clients.items(): try: await client.get_me() - await client.send_message("me", "Health check") + # await client.send_message("me", "Health check") LOGGER.debug("Client %s is healthy", name) except (errors.Flood, errors.FloodWait): LOGGER.warning("Flood error while checking health of client %s", name) From b77aabecd752a73d3ce435d97d2203997c76b8aa Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:57:17 +0530 Subject: [PATCH 2/7] fix for now - AttributeError: 'PeerChannel' object has no attribute 'chat_id' --- pyproject.toml | 3 +++ uv.lock | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d25fbb35..f8a54e73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ Source = "https://github.com/AshokShau/tgmusicbot" [tool.uv] package = true +[tool.uv.sources] +py-tgcalls = { git = "https://github.com/AshokShau/pytgcalls", rev = "master" } + [tool.setuptools] packages = [ "TgMusic", diff --git a/uv.lock b/uv.lock index 43d7ce6b..561ef9a2 100644 --- a/uv.lock +++ b/uv.lock @@ -861,15 +861,12 @@ wheels = [ [[package]] name = "py-tgcalls" version = "2.2.7" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/AshokShau/pytgcalls?rev=master#da3ed7a74d8e8b1c3fa973a7bab5eb5193376dec" } dependencies = [ { name = "aiohttp" }, { name = "deprecation" }, { name = "ntgcalls" }, ] -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/fa/a4238f5696a126431cce7d5cea11b65fa7e80bc467927eb3b786d295cb1e/py_tgcalls-2.2.7-py3-none-any.whl", hash = "sha256:239b7c475a4ac630ddde7ac4693763486c8a48281782e4fe11df069a94f56749", size = 84293, upload-time = "2025-08-30T13:22:38.55Z" }, -] [[package]] name = "py-yt-search" @@ -1291,7 +1288,7 @@ requires-dist = [ { name = "ntgcalls", specifier = "~=2.0.6" }, { name = "pillow", specifier = "~=11.3.0" }, { name = "psutil", specifier = "~=7.0.0" }, - { name = "py-tgcalls", specifier = "~=2.2.7" }, + { name = "py-tgcalls", git = "https://github.com/AshokShau/pytgcalls?rev=master" }, { name = "py-yt-search", specifier = "~=0.3" }, { name = "pycryptodome", specifier = "~=3.23.0" }, { name = "pydantic", specifier = "~=2.11.7" }, From b092ac865ac995f99bebcdef1cab4bc69be06dcb Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:00:55 +0530 Subject: [PATCH 3/7] Install git --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index d4c5fc3e..e5130dc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ wget \ + git \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv From db4b2c16aa9da28c5bee215ebe149321ab1f35c1 Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:17:54 +0530 Subject: [PATCH 4/7] nil --- TgMusic/__init__.py | 5 ++++- TgMusic/core/_health.py | 8 ++++++-- TgMusic/core/_tgcalls.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/TgMusic/__init__.py b/TgMusic/__init__.py index 20671ecc..615a3387 100644 --- a/TgMusic/__init__.py +++ b/TgMusic/__init__.py @@ -80,7 +80,10 @@ async def _initialize_components(self) -> None: await self.call.register_decorators() await super().start() await self.call_manager.start() - await self.health_check.start() + try: + await self.health_check.start() + except Exception as e: + self.logger.error(f"Failed to start health check: {e}") self.logger.info("Bot started successfully") diff --git a/TgMusic/core/_health.py b/TgMusic/core/_health.py index ec89050b..d5e698da 100644 --- a/TgMusic/core/_health.py +++ b/TgMusic/core/_health.py @@ -23,11 +23,15 @@ async def home(self, _: web.Request): }) async def health_check(self, _: web.Request): + import logging + if not self.client: - raise web.HTTPServiceUnavailable(text="Client not initialized") + logging.error("HealthCheck failed: Client not initialized") + raise web.HTTPServiceUnavailable(text="Service temporarily unavailable") if not getattr(self.client, 'is_running', False): - raise web.HTTPServiceUnavailable(text="Client not running") + logging.error("HealthCheck failed: Client not running") + raise web.HTTPServiceUnavailable(text="Service temporarily unavailable") try: await self.client.call.health_check() diff --git a/TgMusic/core/_tgcalls.py b/TgMusic/core/_tgcalls.py index 64289dbd..0d20c975 100644 --- a/TgMusic/core/_tgcalls.py +++ b/TgMusic/core/_tgcalls.py @@ -152,7 +152,8 @@ async def health_check(self) -> None: for name, client in self.pyrogram_clients.items(): try: await client.get_me() - # await client.send_message("me", "Health check") + if not client.is_connected: + raise RuntimeError("Client not connected") LOGGER.debug("Client %s is healthy", name) except (errors.Flood, errors.FloodWait): LOGGER.warning("Flood error while checking health of client %s", name) From ac883a1f8fb836aff67cc2a68cf38687522cb36c Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:49:34 +0530 Subject: [PATCH 5/7] Handle ntgcalls.ConnectionError --- Dockerfile | 6 +++--- TgMusic/core/_tgcalls.py | 19 ++++++++++++++++--- TgMusic/modules/watcher.py | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index e5130dc4..51e8cfb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ - wget \ git \ + curl \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv @@ -15,7 +15,7 @@ COPY . /app/ RUN uv pip install -e . --system # Health check configuration -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5068/health || exit 1 +HEALTHCHECK --interval=60s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:5068/health || exit 1 CMD ["tgmusic"] diff --git a/TgMusic/core/_tgcalls.py b/TgMusic/core/_tgcalls.py index 0d20c975..8323127f 100644 --- a/TgMusic/core/_tgcalls.py +++ b/TgMusic/core/_tgcalls.py @@ -8,7 +8,8 @@ from pathlib import Path from typing import Optional, Union -from ntgcalls import TelegramServerError, ConnectionNotFound +import ntgcalls +from ntgcalls import ConnectionNotFound from pyrogram import Client as PyroClient from pyrogram import errors from pytdbot import Client, types @@ -181,6 +182,12 @@ async def general_handler(_, update: Update, _call=_call): "Cleaning up chat %s after leaving", update.chat_id ) chat_cache.clear_chat(update.chat_id) + elif isinstance(update, ChatUpdate) and update.status.CLOSED_VOICE_CHAT: + LOGGER.debug( + "Cleaning up chat %s after leaving", update.chat_id + ) + chat_cache.clear_chat(update.chat_id) + await self.end(update.chat_id) except Exception as e: LOGGER.error("Error in general handler: %s", e, exc_info=True) @@ -248,13 +255,19 @@ async def play_media( ) return types.Ok() - except (exceptions.NoActiveGroupCall, ConnectionNotFound): + except (exceptions.NoActiveGroupCall, ntgcalls.ConnectionNotFound): return types.Error( code=404, message="No active voice chat found.\n\n" "Please start a voice chat and try again.", ) - except TelegramServerError: + except ntgcalls.ConnectionError as e: + LOGGER.error("Connection error during playback: %s", e) + return types.Error( + code=502, + message="Connection error detected. Please try again later.\nDid you just close the voice chat?", + ) + except ntgcalls.TelegramServerError: LOGGER.warning("Telegram server error during playback") return types.Error( code=502, diff --git a/TgMusic/modules/watcher.py b/TgMusic/modules/watcher.py index 986b96e5..bd86f55f 100644 --- a/TgMusic/modules/watcher.py +++ b/TgMusic/modules/watcher.py @@ -241,6 +241,7 @@ async def new_message(client: Client, update: types.UpdateNewMessage) -> None: if isinstance(content, types.MessageVideoChatStarted): LOGGER.info("Video chat started in %s", chat_id) + await call.end(chat_id) chat_cache.clear_chat(chat_id) await client.sendTextMessage( chat_id, "Video chat started!\nUse /play song name to play a song" From 0666d7e78aa595173ca7f713db6c02e4ba12c427 Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:51:17 +0530 Subject: [PATCH 6/7] Bump deps --- pyproject.toml | 4 ++-- requirements.txt | 18 ------------------ uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index f8a54e73..37d9d85d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,9 @@ dependencies = [ "pycryptodome~=3.23.0", "pydantic~=2.11.7", "pymongo~=4.14.1", - "py-tgcalls~=2.2.7", + "py-tgcalls~=2.2.8", "pytgcrypto~=1.2.11", - "pytdbot~=0.9.6.dev1", + "pytdbot~=0.9.6", "tdjson~=1.8.52", "ujson~=5.11.0", "yt-dlp~=2025.8.27", diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ace54122..00000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -aiofiles~=24.1.0 -cachetools~=6.1.0 -kurigram~=2.2.7 -meval~=2.5 -ntgcalls~=2.0.5 -pillow~=11.3.0 -psutil~=7.0.0 -py-yt-search~=0.3 -pycryptodome~=3.23.0 -pydantic~=2.11.7 -pymongo~=4.13.2 -py-tgcalls~=2.2.5 -pytgcrypto~=1.2.11 -pytdbot~=0.9.6.dev1 -pytz~=2025.2 -tdjson~=1.8.51 -ujson~=5.10.0 -yt-dlp~=2025.7.21 diff --git a/uv.lock b/uv.lock index 561ef9a2..31731fc8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" [[package]] @@ -1088,13 +1088,13 @@ wheels = [ [[package]] name = "pytdbot" -version = "0.9.6.dev1" +version = "0.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aio-pika" }, { name = "deepdiff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/3b/502fc701d124e8614e99a2b4f3414b2eb2a9306e6324c496fb63181f1cf3/pytdbot-0.9.6.dev1.tar.gz", hash = "sha256:a13afe64079245101c6357f8236f5b192103863623092ae355f84bbeed3d34bb", size = 466864, upload-time = "2025-07-17T00:33:34.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/17/f89a861268c3205470c8f9f6df93d0d803faf034d9cc9e5cb5b29d14068f/pytdbot-0.9.6.tar.gz", hash = "sha256:20390f8e043c66d5cb94a8b5f7258dc209bee02000aa9ca864763fc4b0838550", size = 474510, upload-time = "2025-09-02T07:10:50.385Z" } [[package]] name = "pytgcrypto" @@ -1293,7 +1293,7 @@ requires-dist = [ { name = "pycryptodome", specifier = "~=3.23.0" }, { name = "pydantic", specifier = "~=2.11.7" }, { name = "pymongo", specifier = "~=4.14.1" }, - { name = "pytdbot", specifier = "~=0.9.6.dev1" }, + { name = "pytdbot", specifier = "~=0.9.6" }, { name = "pytgcrypto", specifier = "~=1.2.11" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "setuptools", marker = "extra == 'dev'", specifier = "~=80.9.0" }, From 165a7784cabae8c8d459d2cf93a91928c73d8ee5 Mon Sep 17 00:00:00 2001 From: AshokShau <114943948+AshokShau@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:11:26 +0530 Subject: [PATCH 7/7] . --- pyproject.toml | 3 --- uv.lock | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37d9d85d..1d2f62e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,9 +71,6 @@ Source = "https://github.com/AshokShau/tgmusicbot" [tool.uv] package = true -[tool.uv.sources] -py-tgcalls = { git = "https://github.com/AshokShau/pytgcalls", rev = "master" } - [tool.setuptools] packages = [ "TgMusic", diff --git a/uv.lock b/uv.lock index 31731fc8..111bc30f 100644 --- a/uv.lock +++ b/uv.lock @@ -860,13 +860,16 @@ wheels = [ [[package]] name = "py-tgcalls" -version = "2.2.7" -source = { git = "https://github.com/AshokShau/pytgcalls?rev=master#da3ed7a74d8e8b1c3fa973a7bab5eb5193376dec" } +version = "2.2.8" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "deprecation" }, { name = "ntgcalls" }, ] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/fb/7155eb67faab7f6e7b99f6a50faaa2a19a8ca5d1de2d60353a41c1357af5/py_tgcalls-2.2.8-py3-none-any.whl", hash = "sha256:9aef2987c059276a34c1af628192af362d4980627d0206d7500a3799a18aaf89", size = 84448, upload-time = "2025-09-02T09:48:18.826Z" }, +] [[package]] name = "py-yt-search" @@ -1288,7 +1291,7 @@ requires-dist = [ { name = "ntgcalls", specifier = "~=2.0.6" }, { name = "pillow", specifier = "~=11.3.0" }, { name = "psutil", specifier = "~=7.0.0" }, - { name = "py-tgcalls", git = "https://github.com/AshokShau/pytgcalls?rev=master" }, + { name = "py-tgcalls", specifier = "~=2.2.8" }, { name = "py-yt-search", specifier = "~=0.3" }, { name = "pycryptodome", specifier = "~=3.23.0" }, { name = "pydantic", specifier = "~=2.11.7" },