refactor(music): stan serwera, tryb zapętlania i budowanie powiadomień
- GuildMusicState zbiera stan jednego serwera w jedną strukturę. Dotąd było to dziewięć równoległych słowników indeksowanych guild_id (queues, currents, volumes, loop_currents, loop_playlists, last_channels, history, queue_indices, jump_flag), co wymagało pamiętania, że przy każdej zmianie trzeba ruszyć kilka z nich naraz. - LoopMode zastępuje parę booleanów loop_current/loop_playlist, których kombinacja "oba włączone" nie miała sensu, a mimo to była osiągalna. Wartości enuma (off/playlist/single) odpowiadają kontraktowi API panelu. - TrackEmbedFactory zbiera budowanie osadzeń Discorda, powielone dotąd w enqueue_track i play_next. - NotificationChannelResolver opisuje kolejność szukania kanału logów: zapamiętany -> z konfiguracji -> pierwszy zapisywalny na serwerze. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53ffae1663
commit
4cc00618d1
4 files changed
+165
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from zyrarbot.audio.ytdl_source import YTDLSource
|
||||||
|
from zyrarbot.music.loop_mode import LoopMode
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GuildMusicState:
|
||||||
|
"""Stan odtwarzania jednego serwera Discord.
|
||||||
|
|
||||||
|
Kolejka jest nieusuwalna przy odtwarzaniu - pozycję śledzi ``index``,
|
||||||
|
dzięki czemu można swobodnie przeskakiwać i przewijać w tył.
|
||||||
|
"""
|
||||||
|
|
||||||
|
guild_id: int
|
||||||
|
queue: list[YTDLSource] = field(default_factory=list)
|
||||||
|
current: YTDLSource | None = None
|
||||||
|
index: int = -1
|
||||||
|
volume: float = 1.0
|
||||||
|
loop_mode: LoopMode = LoopMode.OFF
|
||||||
|
jump_pending: bool = False
|
||||||
|
notification_channel: discord.abc.Messageable | None = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.queue.clear()
|
||||||
|
self.current = None
|
||||||
|
self.index = -1
|
||||||
|
|
||||||
|
def find(self, track_id: str) -> YTDLSource | None:
|
||||||
|
return next((track for track in self.queue if track.id == track_id), None)
|
||||||
|
|
||||||
|
def position_of(self, track_id: str) -> int:
|
||||||
|
return next((idx for idx, track in enumerate(self.queue) if track.id == track_id), -1)
|
||||||
|
|
||||||
|
def consume_jump(self) -> bool:
|
||||||
|
"""Zwraca i czyści flagę przeskoku ustawioną przez panel webowy."""
|
||||||
|
pending = self.jump_pending
|
||||||
|
self.jump_pending = False
|
||||||
|
return pending
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class LoopMode(Enum):
|
||||||
|
"""Tryb zapętlania w cyklu: wyłączone -> cała kolejka -> jeden utwór."""
|
||||||
|
|
||||||
|
OFF = "off"
|
||||||
|
QUEUE = "playlist"
|
||||||
|
TRACK = "single"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loops_track(self) -> bool:
|
||||||
|
return self is LoopMode.TRACK
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loops_queue(self) -> bool:
|
||||||
|
return self is LoopMode.QUEUE
|
||||||
|
|
||||||
|
def next(self) -> LoopMode:
|
||||||
|
order = (LoopMode.OFF, LoopMode.QUEUE, LoopMode.TRACK)
|
||||||
|
return order[(order.index(self) + 1) % len(order)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return {
|
||||||
|
LoopMode.OFF: "➡️ Tryb zapętlania: **Wyłączone**",
|
||||||
|
LoopMode.QUEUE: "🔁 Tryb zapętlania: **Cała kolejka (playlista)**",
|
||||||
|
LoopMode.TRACK: "🔂 Tryb zapętlania: **Jeden utwór**",
|
||||||
|
}[self]
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from zyrarbot.storage.guild_config_repository import GuildConfigRepository
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationChannelResolver:
|
||||||
|
"""Wskazuje kanał tekstowy, na który trafiają powiadomienia o odtwarzaniu.
|
||||||
|
|
||||||
|
Kolejność: kanał zapamiętany w stanie -> kanał z konfiguracji -> pierwszy
|
||||||
|
kanał serwera, na którym bot może pisać.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot, config: GuildConfigRepository):
|
||||||
|
self._bot = bot
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
def resolve(
|
||||||
|
self, guild_id: int, remembered: discord.abc.Messageable | None
|
||||||
|
) -> discord.abc.Messageable | None:
|
||||||
|
if remembered:
|
||||||
|
return remembered
|
||||||
|
|
||||||
|
saved_id = self._config.get_notification_channel_id(guild_id)
|
||||||
|
if saved_id:
|
||||||
|
channel = self._bot.get_channel(saved_id)
|
||||||
|
if channel:
|
||||||
|
return channel
|
||||||
|
|
||||||
|
return self.first_writable(self._bot.get_guild(guild_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def first_writable(guild: discord.Guild | None) -> discord.TextChannel | None:
|
||||||
|
for channel in NotificationChannelResolver.writable_channels(guild):
|
||||||
|
return channel
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def writable_channels(guild: discord.Guild | None) -> list[discord.TextChannel]:
|
||||||
|
if not guild or not guild.me:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
channel
|
||||||
|
for channel in guild.text_channels
|
||||||
|
if channel.permissions_for(guild.me).send_messages
|
||||||
|
]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
from zyrarbot.audio.ytdl_source import YTDLSource
|
||||||
|
|
||||||
|
AUTHOR_ICON = "https://cdn-icons-png.flaticon.com/512/3844/3844724.png"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackEmbedFactory:
|
||||||
|
"""Buduje osadzenia Discorda opisujące utwór dodany do kolejki lub odtwarzany."""
|
||||||
|
|
||||||
|
def queued(self, track: YTDLSource, *, position: int, from_web: bool) -> discord.Embed:
|
||||||
|
embed = self._base(track, discord.Color.blue())
|
||||||
|
source = "Dodano do kolejki z panelu Web" if from_web else "Dodano do kolejki"
|
||||||
|
embed.set_author(name=source, icon_url=AUTHOR_ICON)
|
||||||
|
embed.set_footer(text=f"Pozycja w kolejce: {position}")
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def now_playing(self, track: YTDLSource, *, position: int, total: int) -> discord.Embed:
|
||||||
|
embed = self._base(track, discord.Color.green(), prefix="▶️")
|
||||||
|
embed.set_footer(text=f"Pozycja w kolejce: {position} / {total}")
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _base(self, track: YTDLSource, color: discord.Color, *, prefix: str = "🎵") -> discord.Embed:
|
||||||
|
title = discord.utils.escape_markdown(track.title or "Nieznany tytuł")
|
||||||
|
url = track.display_url
|
||||||
|
icon = "📁" if track.is_local else prefix
|
||||||
|
description = (
|
||||||
|
f"[Kliknij tutaj, aby otworzyć link]({url})"
|
||||||
|
if track.has_web_link
|
||||||
|
else "Plik lokalny / brak linku"
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{icon} {title}",
|
||||||
|
url=url if track.has_web_link else None,
|
||||||
|
description=description,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
if track.duration:
|
||||||
|
minutes, seconds = divmod(int(track.duration), 60)
|
||||||
|
embed.add_field(name="Czas trwania", value=f"{minutes}:{seconds:02d}", inline=True)
|
||||||
|
return embed
|
||||||
Reference in new issue
Block a user