118 lines
4.3 KiB
Python
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
|