refactor: split music cog by responsibility
This commit is contained in:
8 files changed
+428
-380
No files matched your search
@@ -6,10 +6,14 @@ import discord
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from zyrarbot.audio.track_factory import TrackFactory
|
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.music.music_player import MusicPlayer
|
||||||
from zyrarbot.settings import Settings
|
from zyrarbot.settings import Settings
|
||||||
from zyrarbot.storage.uploads_repository import UploadsRepository
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,11 +36,24 @@ class MusicBot(commands.Bot):
|
|||||||
self.uploads = uploads
|
self.uploads = uploads
|
||||||
self.player: MusicPlayer | None = None
|
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:
|
async def setup_hook(self) -> None:
|
||||||
if self.player is None:
|
if self.player is None:
|
||||||
raise RuntimeError("MusicPlayer musi zostać przypisany przed startem bota")
|
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:
|
if self.settings.guild_id:
|
||||||
guild = discord.Object(id=self.settings.guild_id)
|
guild = discord.Object(id=self.settings.guild_id)
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -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}")
|
||||||
@@ -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}**."
|
|
||||||
)
|
|
||||||
@@ -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)
|
||||||
@@ -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}**."
|
||||||
|
)
|
||||||
@@ -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}**."
|
||||||
|
)
|
||||||
@@ -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.")
|
||||||
Reference in new issue
Block a user