feat: add interactive Discord queue and history actions

This commit is contained in:
mmacierzanka committed 2026-08-21 17:52:56 +02:00
1 parent d08db87739
commit e2099a76b3
3 files changed
+99 -23

No files matched your search

+80
View File
@@ -1,11 +1,14 @@
from __future__ import annotations
import logging
import os
import tempfile
import discord
from discord import app_commands
from discord.ext import commands
from zyrarbot.audio.media_probe import MediaProbe
from zyrarbot.audio.track_factory import TrackFactory
from zyrarbot.music.music_player import MusicPlayer
from zyrarbot.storage.uploads_repository import UploadsRepository
@@ -13,6 +16,29 @@ from zyrarbot.ui.bot.cogs.common import MusicCogBase
logger = logging.getLogger(__name__)
AUTOCOMPLETE_LIMIT = 25
HISTORY_PREVIEW_LIMIT = 25
class HistoryView(discord.ui.View):
def __init__(self, cog: PlaybackCog, guild_id: int):
super().__init__(timeout=900)
self.cog = cog
entries = cog.player.history(guild_id)[:HISTORY_PREVIEW_LIMIT]
options = [
discord.SelectOption(label=entry.title[:100], value=entry.id) for entry in entries
]
if options:
self.add_item(HistorySelect(options))
class HistorySelect(discord.ui.Select):
def __init__(self, options: list[discord.SelectOption]):
super().__init__(placeholder="Wybierz utwór do ponownego odtworzenia", options=options)
async def callback(self, interaction: discord.Interaction) -> None:
view = self.view
if isinstance(view, HistoryView):
await view.cog.replay_entry(interaction, self.values[0])
class PlaybackCog(MusicCogBase, name="Playback"):
@@ -37,6 +63,36 @@ class PlaybackCog(MusicCogBase, name="Playback"):
)
return [app_commands.Choice(name=name, value=name) for name in names]
@app_commands.command(name="upload", description="Prześlij plik audio lub wideo do biblioteki")
@app_commands.describe(file="Plik multimedialny")
async def upload(self, interaction: discord.Interaction, file: discord.Attachment) -> None:
filename = file.filename or ""
if not self._uploads.is_supported_filename(filename):
await interaction.response.send_message(
"❌ Obsługiwane są tylko pliki audio i wideo.", ephemeral=True
)
return
suffix = os.path.splitext(filename)[1]
with tempfile.NamedTemporaryFile(suffix=suffix) as temporary:
await file.save(temporary.name)
if not await MediaProbe().is_media(temporary.name):
await interaction.response.send_message(
"❌ Przesłany plik nie jest prawidłowym plikiem multimedialnym.", ephemeral=True
)
return
with open(temporary.name, "rb") as source:
saved = self._uploads.save(filename, source, interaction.guild.id)
await interaction.response.send_message(f"✅ Dodano plik **{saved}** do biblioteki.")
@app_commands.command(name="deletefile", description="Usuń plik z biblioteki serwera")
@app_commands.describe(filename="Plik z biblioteki")
@app_commands.autocomplete(filename=_uploads_autocomplete)
async def delete_file(self, interaction: discord.Interaction, filename: str) -> None:
if not self._uploads.delete(filename, interaction.guild.id):
await interaction.response.send_message("Nie znaleziono pliku.", ephemeral=True)
return
await interaction.response.send_message(f"✅ Usunięto plik **{filename}**.")
async def _history_autocomplete(
self, interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
@@ -47,6 +103,27 @@ class PlaybackCog(MusicCogBase, name="Playback"):
for entry in matches[:AUTOCOMPLETE_LIMIT]
]
@app_commands.command(name="history", description="Pokaż historię z wyborem utworu do replayu")
async def 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}. {discord.utils.escape_markdown(entry.title)}"
for position, entry in enumerate(entries[:HISTORY_PREVIEW_LIMIT], start=1)
)
await interaction.response.send_message(
embed=discord.Embed(
title="📜 Historia odtwarzania",
description=listing,
color=discord.Color.gold(),
),
view=HistoryView(self, interaction.guild.id),
)
@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:
@@ -88,6 +165,9 @@ class PlaybackCog(MusicCogBase, name="Playback"):
@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:
await self.replay_entry(interaction, track_id)
async def replay_entry(self, interaction: discord.Interaction, track_id: str) -> None:
if not await self._ensure_voice(interaction):
return
await interaction.response.defer()
+16 -23
View File
@@ -9,7 +9,6 @@ from zyrarbot.settings import Settings
from zyrarbot.ui.bot.cogs.common import MusicCogBase
QUEUE_PREVIEW_LIMIT = 30
HISTORY_PREVIEW_LIMIT = 15
AUTOCOMPLETE_LIMIT = 25
@@ -74,28 +73,6 @@ class QueueCog(MusicCogBase, name="Queue"):
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])
)
embed = discord.Embed(
title="📜 Historia odtwarzania",
description=listing,
color=discord.Color.gold(),
)
embed.set_footer(text=f"Ostatnie {min(len(entries), HISTORY_PREVIEW_LIMIT)} utworów")
await interaction.response.send_message(embed=embed)
@app_commands.command(name="restorequeue", description="Przywróć ostatnią zapisaną kolejkę")
async def restore_queue(self, interaction: discord.Interaction) -> None:
count = await self.player.restore_queue(interaction.guild.id)
@@ -132,6 +109,22 @@ class QueueCog(MusicCogBase, name="Queue"):
await self.player.trigger_state_change()
await interaction.response.send_message("▶️ Rozpoczęto wybrany utwór.")
@app_commands.command(name="move", description="Przenieś utwór przed inny utwór")
@app_commands.describe(track_id="Utwór do przeniesienia", before_id="Utwór docelowy")
@app_commands.autocomplete(track_id=_queue_autocomplete, before_id=_queue_autocomplete)
async def move(self, interaction: discord.Interaction, track_id: str, before_id: str) -> None:
state = self.player.state(interaction.guild.id)
if track_id == before_id or not state.find(track_id) or not state.find(before_id):
await interaction.response.send_message(
"❌ Wybierz dwa różne utwory z kolejki.", ephemeral=True
)
return
ids = [track.id for track in state.queue if track.id != track_id]
ids.insert(ids.index(before_id), track_id)
self.player.reorder(interaction.guild.id, ids)
await self.player.trigger_state_change()
await interaction.response.send_message("✅ Zmieniono kolejność kolejki.")
@app_commands.command(
name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce"
)