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