diff --git a/README.md b/README.md index 1925f50..6fc8b4f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ odpowiadające warstwom aplikacji: ``` zyrarbot/ ├── settings.py Settings - konfiguracja ze zmiennych środowiskowych -├── application.py Application - korzeń kompozycji, spina bota i serwer web +├── application.py Application - korzeń kompozycji, spina bota i API ├── storage/ Warstwa trwałości (pliki JSON, katalog uploadów) │ ├── json_file_store.py JsonFileStore │ ├── guild_config_repository.py GuildConfigRepository @@ -52,18 +52,25 @@ zyrarbot/ │ ├── track_embed_factory.py TrackEmbedFactory │ ├── notification_channel_resolver.py NotificationChannelResolver │ └── music_player.py MusicPlayer -├── bot/ Warstwa Discorda -│ ├── client.py MusicBot -│ ├── guild_resolver.py GuildResolver -│ └── cogs/music_cog.py MusicCog -└── web/ Panel webowy (FastAPI) - ├── factory.py create_app - ├── connection_manager.py ConnectionManager - ├── status_service.py StatusService - ├── status_broadcaster.py StatusBroadcaster - ├── dependencies.py wstrzykiwanie zależności z app.state - ├── schemas/ modele żądań - jeden na plik - └── routers/ trasy pogrupowane tematycznie +└── ui/ Warstwa interfejsów użytkownika + ├── api/ API panelu (FastAPI) + │ ├── factory.py mount_api - konfiguracja i routery API + │ ├── connection_manager.py ConnectionManager + │ ├── status_service.py StatusService + │ ├── status_broadcaster.py StatusBroadcaster + │ ├── dependencies.py wstrzykiwanie zależności z app.state + │ ├── requests/ modele żądań - jeden na plik + │ └── routers/ trasy pogrupowane tematycznie + └── bot/ Warstwa Discorda + ├── client.py MusicBot + ├── guild_resolver.py GuildResolver + └── cogs/ komendy i listenery Discorda + ├── common.py wspólne zależności i helpery + ├── playback_cog.py PlaybackCog + ├── queue_cog.py QueueCog + ├── control_cog.py ControlCog + ├── voice_cog.py VoiceCog + └── settings_cog.py SettingsCog ``` ### Zasady @@ -73,5 +80,5 @@ zyrarbot/ dwóch niezależnych ścieżek modyfikujących kolejkę. - **Kolejka jest nieusuwalna, pozycję śledzi indeks** (`GuildMusicState.index`), dzięki czemu działa przeskakiwanie i przewijanie w tył. -- **Zależności wędrują przez konstruktory**, a w warstwie web przez `app.state` - i funkcje z `web/dependencies.py`. Nie ma globalnych singletonów. +- **Zależności wędrują przez konstruktory**, a w API przez `app.state` + i funkcje z `api/dependencies.py`. Nie ma globalnych singletonów. diff --git a/zyrarbot/application.py b/zyrarbot/application.py index 77bcf3a..37cc535 100644 --- a/zyrarbot/application.py +++ b/zyrarbot/application.py @@ -12,8 +12,6 @@ from fastapi.staticfiles import StaticFiles from zyrarbot.audio.media_probe import MediaProbe from zyrarbot.audio.track_factory import TrackFactory -from zyrarbot.ui.bot.client import MusicBot -from zyrarbot.ui.bot.guild_resolver import GuildResolver from zyrarbot.music.music_player import MusicPlayer from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver from zyrarbot.music.track_embed_factory import TrackEmbedFactory @@ -23,6 +21,8 @@ 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__) diff --git a/zyrarbot/ui/__init__.py b/zyrarbot/ui/__init__.py index e16766e..0e0b71e 100644 --- a/zyrarbot/ui/__init__.py +++ b/zyrarbot/ui/__init__.py @@ -1 +1 @@ -"""Komunikacja użytkownika z odtwarzaczem""" \ No newline at end of file +"""Komunikacja użytkownika z odtwarzaczem""" diff --git a/zyrarbot/ui/api/dependencies.py b/zyrarbot/ui/api/dependencies.py index 94188b2..1c4851b 100644 --- a/zyrarbot/ui/api/dependencies.py +++ b/zyrarbot/ui/api/dependencies.py @@ -4,7 +4,6 @@ import discord from fastapi import HTTPException, Request, WebSocket from zyrarbot.audio.track_factory import TrackFactory -from zyrarbot.ui.bot.guild_resolver import GuildResolver from zyrarbot.music.music_player import MusicPlayer from zyrarbot.settings import Settings from zyrarbot.storage.guild_config_repository import GuildConfigRepository @@ -12,6 +11,7 @@ from zyrarbot.storage.uploads_repository import UploadsRepository from zyrarbot.ui.api.connection_manager import ConnectionManager from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster from zyrarbot.ui.api.status_service import StatusService +from zyrarbot.ui.bot.guild_resolver import GuildResolver NO_GUILD_DETAIL = "Bot has no active server context" diff --git a/zyrarbot/ui/api/routers/settings_router.py b/zyrarbot/ui/api/routers/settings_router.py index 624d156..7fced3f 100644 --- a/zyrarbot/ui/api/routers/settings_router.py +++ b/zyrarbot/ui/api/routers/settings_router.py @@ -6,7 +6,6 @@ from typing import Any import discord from fastapi import APIRouter, Depends, HTTPException -from zyrarbot.ui.bot.guild_resolver import GuildResolver from zyrarbot.music.music_player import MusicPlayer from zyrarbot.settings import Settings from zyrarbot.storage.guild_config_repository import GuildConfigRepository @@ -20,6 +19,7 @@ from zyrarbot.ui.api.dependencies import ( from zyrarbot.ui.api.requests.default_plays_request import DefaultPlaysRequest from zyrarbot.ui.api.requests.text_channel_request import TextChannelRequest from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster +from zyrarbot.ui.bot.guild_resolver import GuildResolver logger = logging.getLogger(__name__) diff --git a/zyrarbot/ui/api/routers/voice_router.py b/zyrarbot/ui/api/routers/voice_router.py index 3a1388b..7028af4 100644 --- a/zyrarbot/ui/api/routers/voice_router.py +++ b/zyrarbot/ui/api/routers/voice_router.py @@ -3,7 +3,6 @@ from __future__ import annotations import discord from fastapi import APIRouter, Depends, HTTPException -from zyrarbot.ui.bot.guild_resolver import GuildResolver from zyrarbot.music.music_player import MusicPlayer from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver from zyrarbot.ui.api.dependencies import ( @@ -14,6 +13,7 @@ from zyrarbot.ui.api.dependencies import ( ) from zyrarbot.ui.api.requests.connect_request import ConnectRequest from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster +from zyrarbot.ui.bot.guild_resolver import GuildResolver router = APIRouter(prefix="/api", tags=["voice"]) diff --git a/zyrarbot/ui/api/status_service.py b/zyrarbot/ui/api/status_service.py index 844c491..b15f70c 100644 --- a/zyrarbot/ui/api/status_service.py +++ b/zyrarbot/ui/api/status_service.py @@ -5,9 +5,9 @@ from typing import Any import discord from zyrarbot.audio.ytdl_source import YTDLSource -from zyrarbot.ui.bot.guild_resolver import GuildResolver from zyrarbot.music.music_player import MusicPlayer from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver +from zyrarbot.ui.bot.guild_resolver import GuildResolver HISTORY_PAYLOAD_LIMIT = 20 diff --git a/zyrarbot/ui/bot/cogs/common.py b/zyrarbot/ui/bot/cogs/common.py index ff21d52..94940f9 100644 --- a/zyrarbot/ui/bot/cogs/common.py +++ b/zyrarbot/ui/bot/cogs/common.py @@ -33,9 +33,7 @@ class MusicCogBase(commands.Cog): async def _enqueue_and_reply( self, interaction: discord.Interaction, track, *, prefix: str = "" ) -> None: - result = await self.player.enqueue( - interaction.guild.id, track, channel=interaction.channel - ) + result = await self.player.enqueue(interaction.guild.id, track, channel=interaction.channel) title = discord.utils.escape_markdown(track.title or "Nieznany tytuł") action = "Dodano do kolejki" if result["status"] == "queued" else "Teraz odtwarzane" await interaction.followup.send(f"{action}{prefix}: {title}") diff --git a/zyrarbot/ui/bot/cogs/control_cog.py b/zyrarbot/ui/bot/cogs/control_cog.py index 0f5ceaf..b8dd204 100644 --- a/zyrarbot/ui/bot/cogs/control_cog.py +++ b/zyrarbot/ui/bot/cogs/control_cog.py @@ -1,10 +1,9 @@ from __future__ import annotations -from discord import app_commands import discord +from discord import app_commands from zyrarbot.music.loop_mode import LoopMode -from zyrarbot.music.music_player import MusicPlayer from zyrarbot.ui.bot.cogs.common import MusicCogBase @@ -38,7 +37,9 @@ class ControlCog(MusicCogBase, name="Control"): async def resume(self, interaction: discord.Interaction) -> None: voice_client = interaction.guild.voice_client if not voice_client or not voice_client.is_paused(): - await interaction.response.send_message("Odtwarzanie nie jest wstrzymane.", ephemeral=True) + await interaction.response.send_message( + "Odtwarzanie nie jest wstrzymane.", ephemeral=True + ) return voice_client.resume() await self.player.trigger_state_change() @@ -58,10 +59,14 @@ class ControlCog(MusicCogBase, name="Control"): @app_commands.describe(volume="Poziom głośności (1-100)") async def volume(self, interaction: discord.Interaction, volume: int) -> None: if interaction.guild.voice_client is None: - await interaction.response.send_message("Nie połączono z kanałem głosowym.", ephemeral=True) + await interaction.response.send_message( + "Nie połączono z kanałem głosowym.", ephemeral=True + ) return if not 0 < volume <= 100: - await interaction.response.send_message("Wprowadź wartość z przedziału od 1 do 100.", ephemeral=True) + await interaction.response.send_message( + "Wprowadź wartość z przedziału od 1 do 100.", ephemeral=True + ) return self.player.set_volume(interaction.guild.id, volume / 100) await self.player.trigger_state_change() @@ -71,7 +76,9 @@ class ControlCog(MusicCogBase, name="Control"): async def now_playing(self, interaction: discord.Interaction) -> None: current = self.player.state(interaction.guild.id).current if not current: - await interaction.response.send_message("Nic nie jest obecnie odtwarzane.", ephemeral=True) + await interaction.response.send_message( + "Nic nie jest obecnie odtwarzane.", ephemeral=True + ) return title = discord.utils.escape_markdown(current.title or "") await interaction.response.send_message(f"Teraz odtwarzane: {title}") diff --git a/zyrarbot/ui/bot/cogs/queue_cog.py b/zyrarbot/ui/bot/cogs/queue_cog.py index f4a64c0..aaa8a57 100644 --- a/zyrarbot/ui/bot/cogs/queue_cog.py +++ b/zyrarbot/ui/bot/cogs/queue_cog.py @@ -54,7 +54,9 @@ class QueueCog(MusicCogBase, name="Queue"): for entry in matches[:AUTOCOMPLETE_LIMIT] ] - @app_commands.command(name="history", description="Pokaż historię ostatnio odtwarzanych utworów") + @app_commands.command( + name="history", description="Pokaż historię ostatnio odtwarzanych utworów" + ) async def show_history(self, interaction: discord.Interaction) -> None: entries = self.player.history(interaction.guild.id) if not entries: @@ -70,7 +72,9 @@ class QueueCog(MusicCogBase, name="Queue"): f"📜 Historia odtwarzania (ostatnie {HISTORY_PREVIEW_LIMIT}):\n{listing}" ) - @app_commands.command(name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce") + @app_commands.command( + name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce" + ) @app_commands.describe(index="Pozycja w kolejce (od 1)", count="Nowa liczba odtworzeń (1-999)") async def track_plays_command( self, interaction: discord.Interaction, index: int, count: int diff --git a/zyrarbot/ui/bot/cogs/settings_cog.py b/zyrarbot/ui/bot/cogs/settings_cog.py index cd6ed57..952ec79 100644 --- a/zyrarbot/ui/bot/cogs/settings_cog.py +++ b/zyrarbot/ui/bot/cogs/settings_cog.py @@ -17,30 +17,37 @@ class SettingsCog(MusicCogBase, name="Settings"): super().__init__(bot, player) self._settings = settings - @app_commands.command(name="loop", description="Zmień tryb zapętlania (Wyłączone -> Cała kolejka -> Jeden utwór)") + @app_commands.command( + name="loop", description="Zmień tryb zapętlania (Wyłączone -> Cała kolejka -> Jeden utwór)" + ) async def loop_command(self, interaction: discord.Interaction) -> None: mode = self.player.cycle_loop_mode(interaction.guild.id) await self.player.trigger_state_change() await interaction.response.send_message(mode.label) - @app_commands.command(name="loopplaylist", description="Włącz/wyłącz zapętlanie całej kolejki (tryb playlisty)") + @app_commands.command( + name="loopplaylist", description="Włącz/wyłącz zapętlanie całej kolejki (tryb playlisty)" + ) async def loopplaylist_command(self, interaction: discord.Interaction) -> None: guild_id = interaction.guild.id enabled = not self.player.state(guild_id).loop_mode.loops_queue self.player.set_loop_mode(guild_id, LoopMode.QUEUE if enabled else LoopMode.OFF) await self.player.trigger_state_change() description = ( - "włączone (utwory po zakończeniu wracają na koniec kolejki)" - if enabled - else "wyłączone" + "włączone (utwory po zakończeniu wracają na koniec kolejki)" if enabled else "wyłączone" ) await interaction.response.send_message(f"🔁 Zapętlanie całej kolejki: **{description}**.") - @app_commands.command(name="defaultplays", description="Ustaw domyślną liczbę odtworzeń dla nowych utworów w kolejce") + @app_commands.command( + name="defaultplays", + description="Ustaw domyślną liczbę odtworzeń dla nowych utworów w kolejce", + ) @app_commands.describe(count="Domyślna liczba odtworzeń (1-999)") async def default_plays_command(self, interaction: discord.Interaction, count: int) -> None: if not self._settings.is_valid_plays(count): - await interaction.response.send_message("❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True) + await interaction.response.send_message( + "❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True + ) return self.player.set_default_plays(count) await self.player.trigger_state_change() diff --git a/zyrarbot/ui/bot/cogs/voice_cog.py b/zyrarbot/ui/bot/cogs/voice_cog.py index 1ecf8d3..b6eb063 100644 --- a/zyrarbot/ui/bot/cogs/voice_cog.py +++ b/zyrarbot/ui/bot/cogs/voice_cog.py @@ -29,7 +29,9 @@ class VoiceCog(MusicCogBase, name="Voice"): @discord.app_commands.command(name="leave", description="Odłącz bota od kanału głosowego") async def leave(self, interaction: discord.Interaction) -> None: if interaction.guild.voice_client is None: - await interaction.response.send_message("Nie jestem na kanale głosowym.", ephemeral=True) + await interaction.response.send_message( + "Nie jestem na kanale głosowym.", ephemeral=True + ) return await interaction.guild.voice_client.disconnect() self.player.clear(interaction.guild.id)