Files
zyrarbot/zyrarbot/application.py
T
mmacierzanka d3a5058fde
Backend / Lint i formatowanie (pull_request) Successful in 3s
Backend / Testy i kontrakt API (pull_request) Failing after 2m19s
Backend / Testowy build obrazu (pull_request) Skipped
Formatting, README and other friends
2026-08-21 13:41:38 +02:00

118 lines
4.3 KiB
Python

from __future__ import annotations
import asyncio
import logging
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from zyrarbot.audio.media_probe import MediaProbe
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver
from zyrarbot.music.track_embed_factory import TrackEmbedFactory
from zyrarbot.settings import Settings
from zyrarbot.storage.guild_config_repository import GuildConfigRepository
from zyrarbot.storage.history_repository import HistoryRepository
from zyrarbot.storage.json_file_store import JsonFileStore
from zyrarbot.storage.uploads_repository import UploadsRepository
from zyrarbot.ui.api.factory import mount_api
from zyrarbot.ui.bot.client import MusicBot
from zyrarbot.ui.bot.guild_resolver import GuildResolver
logger = logging.getLogger(__name__)
class Application:
"""Korzeń kompozycji: składa wszystkie zależności i prowadzi bota razem z API."""
def __init__(self, settings: Settings | None = None):
self.settings = settings or Settings.from_env()
self.config = GuildConfigRepository(
JsonFileStore(self.settings.config_file),
fallback_plays=self.settings.default_plays,
)
self.history = HistoryRepository(
JsonFileStore(self.settings.history_file), limit=self.settings.history_limit
)
self.uploads = UploadsRepository(self.settings.uploads_dir)
self.tracks = TrackFactory(self.uploads, MediaProbe())
self.bot = MusicBot(self.settings, self.tracks, self.uploads)
self.guilds = GuildResolver(self.bot)
self.player = MusicPlayer(
bot=self.bot,
track_factory=self.tracks,
history=self.history,
config=self.config,
channels=NotificationChannelResolver(self.bot, self.config),
embeds=TrackEmbedFactory(),
)
self.bot.player = self.player
self.web = FastAPI(title="ŻyrarBOT")
broadcaster = mount_api(
self.web,
settings=self.settings,
player=self.player,
guilds=self.guilds,
tracks=self.tracks,
uploads=self.uploads,
config=self.config,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
self.player.on_state_change = broadcaster.broadcast
broadcaster.start()
logger.info("Panel webowy gotowy, rozgłaszanie stanu uruchomione")
try:
yield
finally:
self.player.on_state_change = None
await broadcaster.stop()
self.web.router.lifespan_context = lifespan
os.makedirs(self.settings.static_dir, exist_ok=True)
self.web.mount(
"/", StaticFiles(directory=self.settings.static_dir, html=True), name="static"
)
async def run(self) -> None:
"""Startuje serwer web, a następnie bota; panel działa też bez Discorda."""
config = uvicorn.Config(
self.web,
host=self.settings.web_host,
port=self.settings.web_port,
log_level="info",
)
server = uvicorn.Server(config)
async with self.bot:
server_task = asyncio.create_task(server.serve())
try:
if not self.settings.discord_token:
raise RuntimeError("Brak DISCORD_TOKEN w środowisku")
await self.bot.start(self.settings.discord_token)
except Exception as error: # noqa: BLE001 - panel ma przetrwać awarię bota
logger.error("Nie udało się wystartować bota Discord: %s", error)
logger.info("Serwer webowy działa dalej...")
await self._wait_for_shutdown(server)
finally:
server.should_exit = True
await server_task
@staticmethod
async def _wait_for_shutdown(server: uvicorn.Server) -> None:
while not server.should_exit:
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
break