feat: add interactive Discord queue and history actions
This commit is contained in:
3 files changed
+99
-23
No files matched your search
@@ -39,12 +39,15 @@ indeksów ręcznie.
|
|||||||
| `/player` | Publiczny panel odtwarzacza z przyciskami sterowania |
|
| `/player` | Publiczny panel odtwarzacza z przyciskami sterowania |
|
||||||
| `/play query` | Dodaje utwór, link lub zapytanie wyszukiwania |
|
| `/play query` | Dodaje utwór, link lub zapytanie wyszukiwania |
|
||||||
| `/playfile filename` | Odtwarza plik z biblioteki serwera |
|
| `/playfile filename` | Odtwarza plik z biblioteki serwera |
|
||||||
|
| `/upload file` | Przesyła i waliduje plik audio/wideo do biblioteki |
|
||||||
|
| `/deletefile filename` | Usuwa wybrany plik z biblioteki |
|
||||||
| `/replay track_id` | Ponownie odtwarza utwór z historii |
|
| `/replay track_id` | Ponownie odtwarza utwór z historii |
|
||||||
| `/queue` | Pokazuje kolejkę w embedzie |
|
| `/queue` | Pokazuje kolejkę w embedzie |
|
||||||
| `/history` | Pokazuje historię odtwarzania |
|
| `/history` | Pokazuje historię odtwarzania |
|
||||||
| `/restorequeue` | Przywraca ostatnią zapisaną kolejkę |
|
| `/restorequeue` | Przywraca ostatnią zapisaną kolejkę |
|
||||||
| `/jump track_id` | Rozpoczyna wybrany utwór z kolejki |
|
| `/jump track_id` | Rozpoczyna wybrany utwór z kolejki |
|
||||||
| `/remove track_id` | Usuwa wybrany utwór z kolejki |
|
| `/remove track_id` | Usuwa wybrany utwór z kolejki |
|
||||||
|
| `/move track_id before_id` | Przenosi utwór przed wybrany utwór docelowy |
|
||||||
| `/trackplays track_id count` | Ustawia liczbę odtworzeń utworu |
|
| `/trackplays track_id count` | Ustawia liczbę odtworzeń utworu |
|
||||||
| `/pause`, `/resume` | Wstrzymuje lub wznawia odtwarzanie |
|
| `/pause`, `/resume` | Wstrzymuje lub wznawia odtwarzanie |
|
||||||
| `/skip` | Pomija bieżący utwór |
|
| `/skip` | Pomija bieżący utwór |
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from zyrarbot.audio.media_probe import MediaProbe
|
||||||
from zyrarbot.audio.track_factory import TrackFactory
|
from zyrarbot.audio.track_factory import TrackFactory
|
||||||
from zyrarbot.music.music_player import MusicPlayer
|
from zyrarbot.music.music_player import MusicPlayer
|
||||||
from zyrarbot.storage.uploads_repository import UploadsRepository
|
from zyrarbot.storage.uploads_repository import UploadsRepository
|
||||||
@@ -13,6 +16,29 @@ from zyrarbot.ui.bot.cogs.common import MusicCogBase
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
AUTOCOMPLETE_LIMIT = 25
|
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"):
|
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]
|
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(
|
async def _history_autocomplete(
|
||||||
self, interaction: discord.Interaction, current: str
|
self, interaction: discord.Interaction, current: str
|
||||||
) -> list[app_commands.Choice[str]]:
|
) -> list[app_commands.Choice[str]]:
|
||||||
@@ -47,6 +103,27 @@ class PlaybackCog(MusicCogBase, name="Playback"):
|
|||||||
for entry in matches[:AUTOCOMPLETE_LIMIT]
|
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.command(name="play", description="Odtwórz utwór z YouTube")
|
||||||
@app_commands.describe(query="Link YouTube lub zapytanie wyszukiwania")
|
@app_commands.describe(query="Link YouTube lub zapytanie wyszukiwania")
|
||||||
async def play(self, interaction: discord.Interaction, query: str) -> None:
|
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.describe(track_id="Wybierz utwór z historii odtwarzania")
|
||||||
@app_commands.autocomplete(track_id=_history_autocomplete)
|
@app_commands.autocomplete(track_id=_history_autocomplete)
|
||||||
async def replay(self, interaction: discord.Interaction, track_id: str) -> None:
|
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):
|
if not await self._ensure_voice(interaction):
|
||||||
return
|
return
|
||||||
await interaction.response.defer()
|
await interaction.response.defer()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from zyrarbot.settings import Settings
|
|||||||
from zyrarbot.ui.bot.cogs.common import MusicCogBase
|
from zyrarbot.ui.bot.cogs.common import MusicCogBase
|
||||||
|
|
||||||
QUEUE_PREVIEW_LIMIT = 30
|
QUEUE_PREVIEW_LIMIT = 30
|
||||||
HISTORY_PREVIEW_LIMIT = 15
|
|
||||||
AUTOCOMPLETE_LIMIT = 25
|
AUTOCOMPLETE_LIMIT = 25
|
||||||
|
|
||||||
|
|
||||||
@@ -74,28 +73,6 @@ class QueueCog(MusicCogBase, name="Queue"):
|
|||||||
for entry in matches[:AUTOCOMPLETE_LIMIT]
|
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ę")
|
@app_commands.command(name="restorequeue", description="Przywróć ostatnią zapisaną kolejkę")
|
||||||
async def restore_queue(self, interaction: discord.Interaction) -> None:
|
async def restore_queue(self, interaction: discord.Interaction) -> None:
|
||||||
count = await self.player.restore_queue(interaction.guild.id)
|
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 self.player.trigger_state_change()
|
||||||
await interaction.response.send_message("▶️ Rozpoczęto wybrany utwór.")
|
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(
|
@app_commands.command(
|
||||||
name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce"
|
name="trackplays", description="Zmień liczbę odtworzeń dla utworu w kolejce"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user