refactor: split music cog by responsibility

This commit is contained in:
mmacierzanka committed 2026-08-21 13:16:19 +02:00
1 parent d706317dc8
commit d31747e80b
8 files changed
+428 -380

No files matched your search

+19 -2
View File
@@ -6,10 +6,14 @@ import discord
from discord.ext import commands
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.ui.bot.cogs.music_cog import MusicCog
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.settings import Settings
from zyrarbot.storage.uploads_repository import UploadsRepository
from zyrarbot.ui.bot.cogs.control_cog import ControlCog
from zyrarbot.ui.bot.cogs.playback_cog import PlaybackCog
from zyrarbot.ui.bot.cogs.queue_cog import QueueCog
from zyrarbot.ui.bot.cogs.settings_cog import SettingsCog
from zyrarbot.ui.bot.cogs.voice_cog import VoiceCog
logger = logging.getLogger(__name__)
@@ -32,11 +36,24 @@ class MusicBot(commands.Bot):
self.uploads = uploads
self.player: MusicPlayer | None = None
def _create_music_cogs(self) -> tuple[commands.Cog, ...]:
if self.player is None:
raise RuntimeError("MusicPlayer musi zostać przypisany przed utworzeniem Cogów")
return (
PlaybackCog(self, self.player, self.tracks, self.uploads),
QueueCog(self, self.player, self.settings),
ControlCog(self, self.player),
VoiceCog(self, self.player),
SettingsCog(self, self.player, self.settings),
)
async def setup_hook(self) -> None:
if self.player is None:
raise RuntimeError("MusicPlayer musi zostać przypisany przed startem bota")
await self.add_cog(MusicCog(self, self.player, self.tracks, self.uploads, self.settings))
for cog in self._create_music_cogs():
await self.add_cog(cog)
if self.settings.guild_id:
guild = discord.Object(id=self.settings.guild_id)
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import discord
from discord.ext import commands
from zyrarbot.music.music_player import MusicPlayer
class MusicCogBase(commands.Cog):
"""Wspólne zależności i pomocnicze operacje dla muzycznych Cogów."""
def __init__(self, bot: commands.Bot, player: MusicPlayer):
self.bot = bot
self.player = player
async def _ensure_voice(self, interaction: discord.Interaction) -> bool:
if not interaction.user.voice:
await interaction.response.send_message(
"Nie jesteś połączony z kanałem głosowym.", ephemeral=True
)
return False
voice_client = interaction.guild.voice_client
if voice_client is None:
await interaction.user.voice.channel.connect()
elif voice_client.channel != interaction.user.voice.channel:
await interaction.response.send_message(
"Jestem już połączony z innym kanałem głosowym.", ephemeral=True
)
return False
return True
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
)
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}")
+77
View File
@@ -0,0 +1,77 @@
from __future__ import annotations
from discord import app_commands
import discord
from zyrarbot.music.loop_mode import LoopMode
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.ui.bot.cogs.common import MusicCogBase
class ControlCog(MusicCogBase, name="Control"):
"""Komendy bezpośredniego sterowania odtwarzaniem."""
@app_commands.command(name="skip", description="Pomiń aktualny utwór")
async def skip(self, interaction: discord.Interaction) -> None:
guild_id = interaction.guild.id
state = self.player.state(guild_id)
if state.loop_mode.loops_track:
state.loop_mode = LoopMode.OFF
if not self.player.is_active(guild_id):
await interaction.response.send_message("Nic nie jest odtwarzane.", ephemeral=True)
return
interaction.guild.voice_client.stop()
await self.player.trigger_state_change()
await interaction.response.send_message("Pominięto aktualny utwór.")
@app_commands.command(name="pause", description="Wstrzymaj odtwarzanie")
async def pause(self, interaction: discord.Interaction) -> None:
voice_client = interaction.guild.voice_client
if not voice_client or not voice_client.is_playing():
await interaction.response.send_message("Nic nie jest odtwarzane.", ephemeral=True)
return
voice_client.pause()
await self.player.trigger_state_change()
await interaction.response.send_message("Odtwarzanie wstrzymane.")
@app_commands.command(name="resume", description="Wznów odtwarzanie")
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)
return
voice_client.resume()
await self.player.trigger_state_change()
await interaction.response.send_message("Odtwarzanie wznowione.")
@app_commands.command(name="stop", description="Zatrzymaj odtwarzanie i wyczyść kolejkę")
async def stop(self, interaction: discord.Interaction) -> None:
guild_id = interaction.guild.id
voice_client = interaction.guild.voice_client
if voice_client and voice_client.is_playing():
voice_client.stop()
self.player.clear(guild_id)
await self.player.trigger_state_change()
await interaction.response.send_message("Zatrzymano odtwarzanie i wyczyszczono kolejkę.")
@app_commands.command(name="volume", description="Zmień głośność odtwarzacza")
@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)
return
if not 0 < volume <= 100:
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()
await interaction.response.send_message(f"Zmieniono głośność na {volume}%")
@app_commands.command(name="nowplaying", description="Pokaż aktualnie odtwarzany utwór")
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)
return
title = discord.utils.escape_markdown(current.title or "")
await interaction.response.send_message(f"Teraz odtwarzane: {title}")
-378
View File
@@ -1,378 +0,0 @@
from __future__ import annotations
import logging
import discord
from discord import app_commands
from discord.ext import commands
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.music.loop_mode import LoopMode
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.settings import Settings
from zyrarbot.storage.uploads_repository import UploadsRepository
logger = logging.getLogger(__name__)
QUEUE_PREVIEW_LIMIT = 30
HISTORY_PREVIEW_LIMIT = 15
AUTOCOMPLETE_LIMIT = 25
class MusicCog(commands.Cog, name="MusicBot"):
"""Komendy ukośnikowe sterujące odtwarzaczem. Cała logika żyje w ``MusicPlayer``."""
def __init__(
self,
bot: commands.Bot,
player: MusicPlayer,
tracks: TrackFactory,
uploads: UploadsRepository,
settings: Settings,
):
self.bot = bot
self.player = player
self._tracks = tracks
self._uploads = uploads
self._settings = settings
# --- Zdarzenia ----------------------------------------------------------
@commands.Cog.listener()
async def on_voice_state_update(
self,
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState,
) -> None:
is_bot_leaving = (
self.bot.user
and member.id == self.bot.user.id
and before.channel is not None
and after.channel is None
)
if is_bot_leaving:
self.player.clear(before.channel.guild.id)
await self.player.trigger_state_change()
# --- Pomocnicze ---------------------------------------------------------
async def _ensure_voice(self, interaction: discord.Interaction) -> bool:
if not interaction.user.voice:
await interaction.response.send_message(
"Nie jesteś połączony z kanałem głosowym.", ephemeral=True
)
return False
voice_client = interaction.guild.voice_client
if voice_client is None:
await interaction.user.voice.channel.connect()
elif voice_client.channel != interaction.user.voice.channel:
await interaction.response.send_message(
"Jestem już połączony z innym kanałem głosowym.", ephemeral=True
)
return False
return True
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)
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}")
# --- Odtwarzanie --------------------------------------------------------
@app_commands.command(name="play", description="Odtwórz utwór z YouTube")
@app_commands.describe(query="Link YouTube lub zapytanie wyszukiwania")
async def play(self, interaction: discord.Interaction, query: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
try:
track = await self._tracks.resolve(query, loop=self.bot.loop)
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001 - komunikat trafia do użytkownika
logger.error("Komenda /play nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
async def _uploads_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
names = self._uploads.search(current, limit=AUTOCOMPLETE_LIMIT)
return [app_commands.Choice(name=name, value=name) for name in names]
@app_commands.command(name="playfile", description="Odtwórz plik przesłany na serwer")
@app_commands.describe(filename="Wybierz plik z listy przesłanych")
@app_commands.autocomplete(filename=_uploads_autocomplete)
async def playfile(self, interaction: discord.Interaction, filename: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
if not self._uploads.exists(filename):
await interaction.followup.send("Nie znaleziono pliku na serwerze.", ephemeral=True)
return
try:
track = await self._tracks.from_file(self._uploads.resolve(filename))
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001
logger.error("Komenda /playfile nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
# --- Kolejka i historia -------------------------------------------------
@app_commands.command(name="queue", description="Pokaż aktualną kolejkę")
async def show_queue(self, interaction: discord.Interaction) -> None:
state = self.player.state(interaction.guild.id)
if not state.queue:
await interaction.response.send_message("Kolejka jest pusta.", ephemeral=True)
return
lines: list[str] = []
if state.current and not 0 <= state.index < len(state.queue):
title = discord.utils.escape_markdown(state.current.title or "")
plays = self.player.plays_left(state.current)
lines.append(
f"▶️ **{title}** (obecnie odtwarzane - ostatnie odtworzenie) [🔁 {plays}]\n---"
)
for position, track in enumerate(state.queue):
is_current = position == state.index
prefix = "▶️ **" if is_current else f"{position + 1}. "
suffix = "** (obecnie odtwarzane)" if is_current else ""
title = discord.utils.escape_markdown(track.title or "")
lines.append(f"{prefix}{title}{suffix} [🔁 {self.player.plays_left(track)}]")
preview = "\n".join(lines[:QUEUE_PREVIEW_LIMIT])
if len(lines) > QUEUE_PREVIEW_LIMIT:
preview += f"\n... i {len(lines) - QUEUE_PREVIEW_LIMIT} więcej."
await interaction.response.send_message(f"Aktualna kolejka:\n{preview}")
async def _history_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
entries = self.player.history(interaction.guild.id)
matches = [entry for entry in entries if current.lower() in entry.title.lower()]
return [
app_commands.Choice(name=entry.title[:100], value=entry.id)
for entry in matches[:AUTOCOMPLETE_LIMIT]
]
@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:
await interaction.response.send_message(
"Historia odtwarzania jest pusta.", ephemeral=True
)
return
listing = "\n".join(
f"{position + 1}. {discord.utils.escape_markdown(entry.title)}"
for position, entry in enumerate(entries[:HISTORY_PREVIEW_LIMIT])
)
await interaction.response.send_message(
f"📜 Historia odtwarzania (ostatnie {HISTORY_PREVIEW_LIMIT}):\n{listing}"
)
@app_commands.command(name="replay", description="Odtwórz ponownie utwór z historii")
@app_commands.describe(track_id="Wybierz utwór z historii odtwarzania")
@app_commands.autocomplete(track_id=_history_autocomplete)
async def replay(self, interaction: discord.Interaction, track_id: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
guild_id = interaction.guild.id
entries = self.player.history(guild_id)
entry = self.player.find_history_entry(guild_id, track_id)
if entry is None and track_id.isdigit():
position = int(track_id) - 1
if 0 <= position < len(entries):
entry = entries[position]
if entry is None:
await interaction.followup.send("Nie znaleziono utworu w historii.", ephemeral=True)
return
try:
if entry.is_local:
filepath = entry.replay_source or ""
if not self._uploads.exists(filepath):
await interaction.followup.send(
"Plik lokalny z historii już nie istnieje na serwerze.", ephemeral=True
)
return
track = await self._tracks.from_file(
self._uploads.resolve(filepath), title=entry.title
)
else:
track = await self._tracks.from_url(entry.url or "", loop=self.bot.loop)
await self._enqueue_and_reply(interaction, track, prefix=" z historii")
except Exception as error: # noqa: BLE001
logger.error("Komenda /replay nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
# --- Sterowanie ---------------------------------------------------------
@app_commands.command(name="skip", description="Pomiń aktualny utwór")
async def skip(self, interaction: discord.Interaction) -> None:
guild_id = interaction.guild.id
state = self.player.state(guild_id)
if state.loop_mode.loops_track:
state.loop_mode = LoopMode.OFF
if not self.player.is_active(guild_id):
await interaction.response.send_message("Nic nie jest odtwarzane.", ephemeral=True)
return
interaction.guild.voice_client.stop()
await self.player.trigger_state_change()
await interaction.response.send_message("Pominięto aktualny utwór.")
@app_commands.command(name="pause", description="Wstrzymaj odtwarzanie")
async def pause(self, interaction: discord.Interaction) -> None:
voice_client = interaction.guild.voice_client
if not voice_client or not voice_client.is_playing():
await interaction.response.send_message("Nic nie jest odtwarzane.", ephemeral=True)
return
voice_client.pause()
await self.player.trigger_state_change()
await interaction.response.send_message("Odtwarzanie wstrzymane.")
@app_commands.command(name="resume", description="Wznów odtwarzanie")
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
)
return
voice_client.resume()
await self.player.trigger_state_change()
await interaction.response.send_message("Odtwarzanie wznowione.")
@app_commands.command(name="stop", description="Zatrzymaj odtwarzanie i wyczyść kolejkę")
async def stop(self, interaction: discord.Interaction) -> None:
guild_id = interaction.guild.id
voice_client = interaction.guild.voice_client
if voice_client and voice_client.is_playing():
voice_client.stop()
self.player.clear(guild_id)
await self.player.trigger_state_change()
await interaction.response.send_message("Zatrzymano odtwarzanie i wyczyszczono kolejkę.")
@app_commands.command(name="volume", description="Zmień głośność odtwarzacza")
@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
)
return
if not 0 < volume <= 100:
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()
await interaction.response.send_message(f"Zmieniono głośność na {volume}%")
@app_commands.command(name="nowplaying", description="Pokaż aktualnie odtwarzany utwór")
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
)
return
title = discord.utils.escape_markdown(current.title or "")
await interaction.response.send_message(f"Teraz odtwarzane: {title}")
@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
)
return
await interaction.guild.voice_client.disconnect()
self.player.clear(interaction.guild.id)
await interaction.response.send_message("Rozłączono z kanału głosowego.")
# --- Zapętlanie i limity odtworzeń --------------------------------------
@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)",
)
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"
)
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.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
)
return
self.player.set_default_plays(count)
await self.player.trigger_state_change()
await interaction.response.send_message(
f"✅ Domyślna liczba odtworzeń dla nowych utworów została ustawiona na: **{count}**."
)
@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
) -> None:
queue = self.player.state(interaction.guild.id).queue
if not 1 <= index <= len(queue):
await interaction.response.send_message(
"❌ Nieprawidłowy numer utworu w kolejce.", ephemeral=True
)
return
if not self._settings.is_valid_plays(count):
await interaction.response.send_message(
"❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True
)
return
track = queue[index - 1]
track.plays_left = count
await self.player.trigger_state_change()
title = discord.utils.escape_markdown(track.title or "")
await interaction.response.send_message(
f"✅ Ustawiono liczbę odtworzeń dla utworu **{title}** na **{count}**."
)
+111
View File
@@ -0,0 +1,111 @@
from __future__ import annotations
import logging
import discord
from discord import app_commands
from discord.ext import commands
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.storage.uploads_repository import UploadsRepository
from zyrarbot.ui.bot.cogs.common import MusicCogBase
logger = logging.getLogger(__name__)
AUTOCOMPLETE_LIMIT = 25
class PlaybackCog(MusicCogBase, name="Playback"):
"""Komendy dodawania i ponownego odtwarzania utworów."""
def __init__(
self,
bot: commands.Bot,
player: MusicPlayer,
tracks: TrackFactory,
uploads: UploadsRepository,
):
super().__init__(bot, player)
self._tracks = tracks
self._uploads = uploads
async def _uploads_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
names = self._uploads.search(current, limit=AUTOCOMPLETE_LIMIT)
return [app_commands.Choice(name=name, value=name) for name in names]
async def _history_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
entries = self.player.history(interaction.guild.id)
matches = [entry for entry in entries if current.lower() in entry.title.lower()]
return [
app_commands.Choice(name=entry.title[:100], value=entry.id)
for entry in matches[:AUTOCOMPLETE_LIMIT]
]
@app_commands.command(name="play", description="Odtwórz utwór z YouTube")
@app_commands.describe(query="Link YouTube lub zapytanie wyszukiwania")
async def play(self, interaction: discord.Interaction, query: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
try:
track = await self._tracks.resolve(query, loop=self.bot.loop)
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001
logger.error("Komenda /play nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
@app_commands.command(name="playfile", description="Odtwórz plik przesłany na serwer")
@app_commands.describe(filename="Wybierz plik z listy przesłanych")
@app_commands.autocomplete(filename=_uploads_autocomplete)
async def playfile(self, interaction: discord.Interaction, filename: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
if not self._uploads.exists(filename):
await interaction.followup.send("Nie znaleziono pliku na serwerze.", ephemeral=True)
return
try:
track = await self._tracks.from_file(self._uploads.resolve(filename))
await self._enqueue_and_reply(interaction, track)
except Exception as error: # noqa: BLE001
logger.error("Komenda /playfile nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
@app_commands.command(name="replay", description="Odtwórz ponownie utwór z historii")
@app_commands.describe(track_id="Wybierz utwór z historii odtwarzania")
@app_commands.autocomplete(track_id=_history_autocomplete)
async def replay(self, interaction: discord.Interaction, track_id: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
guild_id = interaction.guild.id
entries = self.player.history(guild_id)
entry = self.player.find_history_entry(guild_id, track_id)
if entry is None and track_id.isdigit():
position = int(track_id) - 1
if 0 <= position < len(entries):
entry = entries[position]
if entry is None:
await interaction.followup.send("Nie znaleziono utworu w historii.", ephemeral=True)
return
try:
if entry.is_local:
filepath = entry.replay_source or ""
if not self._uploads.exists(filepath):
await interaction.followup.send(
"Plik lokalny z historii już nie istnieje na serwerze.", ephemeral=True
)
return
track = await self._tracks.from_file(
self._uploads.resolve(filepath), title=entry.title
)
else:
track = await self._tracks.from_url(entry.url or "", loop=self.bot.loop)
await self._enqueue_and_reply(interaction, track, prefix=" z historii")
except Exception as error: # noqa: BLE001
logger.error("Komenda /replay nie powiodła się: %s", error)
await interaction.followup.send(f"Wystąpił błąd: {error}", ephemeral=True)
+95
View File
@@ -0,0 +1,95 @@
from __future__ import annotations
import discord
from discord import app_commands
from discord.ext import commands
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.settings import Settings
from zyrarbot.ui.bot.cogs.common import MusicCogBase
QUEUE_PREVIEW_LIMIT = 30
HISTORY_PREVIEW_LIMIT = 15
AUTOCOMPLETE_LIMIT = 25
class QueueCog(MusicCogBase, name="Queue"):
"""Komendy podglądu kolejki i historii odtwarzania."""
def __init__(self, bot: commands.Bot, player: MusicPlayer, settings: Settings):
super().__init__(bot, player)
self._settings = settings
@app_commands.command(name="queue", description="Pokaż aktualną kolejkę")
async def show_queue(self, interaction: discord.Interaction) -> None:
state = self.player.state(interaction.guild.id)
if not state.queue:
await interaction.response.send_message("Kolejka jest pusta.", ephemeral=True)
return
lines: list[str] = []
if state.current and not 0 <= state.index < len(state.queue):
title = discord.utils.escape_markdown(state.current.title or "")
plays = self.player.plays_left(state.current)
lines.append(
f"▶️ **{title}** (obecnie odtwarzane - ostatnie odtworzenie) [🔁 {plays}]\n---"
)
for position, track in enumerate(state.queue):
is_current = position == state.index
prefix = "▶️ **" if is_current else f"{position + 1}. "
suffix = "** (obecnie odtwarzane)" if is_current else ""
title = discord.utils.escape_markdown(track.title or "")
lines.append(f"{prefix}{title}{suffix} [🔁 {self.player.plays_left(track)}]")
preview = "\n".join(lines[:QUEUE_PREVIEW_LIMIT])
if len(lines) > QUEUE_PREVIEW_LIMIT:
preview += f"\n... i {len(lines) - QUEUE_PREVIEW_LIMIT} więcej."
await interaction.response.send_message(f"Aktualna kolejka:\n{preview}")
async def _history_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
entries = self.player.history(interaction.guild.id)
matches = [entry for entry in entries if current.lower() in entry.title.lower()]
return [
app_commands.Choice(name=entry.title[:100], value=entry.id)
for entry in matches[:AUTOCOMPLETE_LIMIT]
]
@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:
await interaction.response.send_message(
"Historia odtwarzania jest pusta.", ephemeral=True
)
return
listing = "\n".join(
f"{position + 1}. {discord.utils.escape_markdown(entry.title)}"
for position, entry in enumerate(entries[:HISTORY_PREVIEW_LIMIT])
)
await interaction.response.send_message(
f"📜 Historia odtwarzania (ostatnie {HISTORY_PREVIEW_LIMIT}):\n{listing}"
)
@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
) -> None:
queue = self.player.state(interaction.guild.id).queue
if not 1 <= index <= len(queue):
await interaction.response.send_message(
"❌ Nieprawidłowy numer utworu w kolejce.", ephemeral=True
)
return
if not self._settings.is_valid_plays(count):
await interaction.response.send_message(
"❌ Liczba odtworzeń musi być z przedziału 1-999.", ephemeral=True
)
return
track = queue[index - 1]
track.plays_left = count
await self.player.trigger_state_change()
title = discord.utils.escape_markdown(track.title or "")
await interaction.response.send_message(
f"✅ Ustawiono liczbę odtworzeń dla utworu **{title}** na **{count}**."
)
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
import discord
from discord import app_commands
from discord.ext import commands
from zyrarbot.music.loop_mode import LoopMode
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.settings import Settings
from zyrarbot.ui.bot.cogs.common import MusicCogBase
class SettingsCog(MusicCogBase, name="Settings"):
"""Komendy zmieniające ustawienia odtwarzacza."""
def __init__(self, bot: commands.Bot, player: MusicPlayer, settings: 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)")
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)")
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"
)
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.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)
return
self.player.set_default_plays(count)
await self.player.trigger_state_change()
await interaction.response.send_message(
f"✅ Domyślna liczba odtworzeń dla nowych utworów została ustawiona na: **{count}**."
)
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
import discord
from discord.ext import commands
from zyrarbot.ui.bot.cogs.common import MusicCogBase
class VoiceCog(MusicCogBase, name="Voice"):
"""Obsługa zdarzeń i rozłączania z kanałów głosowych."""
@commands.Cog.listener()
async def on_voice_state_update(
self,
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState,
) -> None:
is_bot_leaving = (
self.bot.user
and member.id == self.bot.user.id
and before.channel is not None
and after.channel is None
)
if is_bot_leaving:
self.player.clear(before.channel.guild.id)
await self.player.trigger_state_change()
@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)
return
await interaction.guild.voice_client.disconnect()
self.player.clear(interaction.guild.id)
await interaction.response.send_message("Rozłączono z kanału głosowego.")