180 lines
7.2 KiB
Python
180 lines
7.2 KiB
Python
import wavelink
|
|
import discord
|
|
import logging
|
|
from discord.ext import commands
|
|
from ..shared.constants import LOOP_TRACK, LOOP_QUEUE, MODE_RADIO, MODE_MUSIC, C_MAIN
|
|
from ..shared.utils import fmt_duration
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class MusicEvents:
|
|
"""Mixin: wavelink + Discord events that belong to the music player."""
|
|
|
|
# ── channel status helpers ────────────────────────────────────────────────
|
|
async def _set_channel_status(
|
|
self,
|
|
player: wavelink.Player,
|
|
track: wavelink.Playable | None = None,
|
|
*,
|
|
title: str | None = None,
|
|
):
|
|
"""Set (or clear) the voice channel status. Pass *title* for radio; otherwise uses track.title."""
|
|
channel = player.channel
|
|
if channel is None:
|
|
return
|
|
try:
|
|
if title is not None:
|
|
status = f"📻 {title}"[:500]
|
|
elif track is not None:
|
|
status = f"🎵 {track.title}"[:500]
|
|
else:
|
|
status = None
|
|
route = discord.http.Route(
|
|
"PUT",
|
|
"/channels/{channel_id}/voice-status",
|
|
channel_id=channel.id,
|
|
)
|
|
await self.bot.http.request(route, json={"status": status})
|
|
except Exception:
|
|
log.warning(f"Failed to set voice channel status in {player.guild.id}", exc_info=True)
|
|
|
|
async def _clear_channel_status(self, guild_id: int):
|
|
"""Clear voice channel status after the bot was disconnected."""
|
|
channel_id = self._voice_channels.pop(guild_id, None)
|
|
if channel_id is None:
|
|
return
|
|
try:
|
|
route = discord.http.Route(
|
|
"PUT",
|
|
"/channels/{channel_id}/voice-status",
|
|
channel_id=channel_id,
|
|
)
|
|
await self.bot.http.request(route, json={"status": None})
|
|
except Exception:
|
|
log.warning(f"Failed to clear voice channel status in guild {guild_id}", exc_info=True)
|
|
|
|
# ── connect lavalink ──────────────────────────────────────────────────────
|
|
@commands.Cog.listener()
|
|
async def on_ready(self):
|
|
node = wavelink.Node(uri="http://127.0.0.1:2333", password="yuukabot")
|
|
await wavelink.Pool.connect(nodes=[node], client=self.bot)
|
|
log.info("Wavelink connected to Lavalink")
|
|
|
|
# ── voice state update → clean up on disconnect ───────────────────────────
|
|
@commands.Cog.listener()
|
|
async def on_voice_state_update(
|
|
self,
|
|
member: discord.Member,
|
|
before: discord.VoiceState,
|
|
after: discord.VoiceState,
|
|
):
|
|
if member.id != self.bot.user.id:
|
|
return
|
|
# Bot left a channel (kicked, moved out, or disconnected)
|
|
if before.channel is not None and after.channel is None:
|
|
guild_id = member.guild.id
|
|
st = self._states.pop(guild_id, None)
|
|
if st:
|
|
st.queue.clear()
|
|
self._locks.pop(guild_id, None)
|
|
self._radio_channels.pop(guild_id, None)
|
|
self._music_channels.pop(guild_id, None)
|
|
self._cancel_icy_poller(guild_id)
|
|
await self._clear_channel_status(guild_id)
|
|
|
|
# ── track end → advance queue ─────────────────────────────────────────────
|
|
@commands.Cog.listener()
|
|
async def on_wavelink_track_end(self, payload: wavelink.TrackEndEventPayload):
|
|
player: wavelink.Player = payload.player
|
|
if player is None:
|
|
return
|
|
|
|
st = self.state(player.guild.id)
|
|
|
|
# Ignore stale events (e.g. track_end for an old radio stream after a switch)
|
|
if not self._is_current_track(st, payload.track):
|
|
return
|
|
|
|
if st.loop == LOOP_TRACK and st.current:
|
|
await player.play(st.current)
|
|
await self._set_channel_status(player, st.current)
|
|
return
|
|
|
|
if st.loop == LOOP_QUEUE and st.current:
|
|
st.queue.append(st.current)
|
|
|
|
if st.current:
|
|
st.push_history(st.current)
|
|
|
|
if st.queue:
|
|
next_track = st.queue.popleft()
|
|
st.current = next_track
|
|
await player.play(next_track)
|
|
await self._set_channel_status(player, next_track)
|
|
elif st.autoplay:
|
|
await player.set_autoplay(wavelink.AutoPlayMode.enabled)
|
|
else:
|
|
if st.mode == MODE_RADIO:
|
|
await self._notify_radio_drop(player.guild.id)
|
|
st.current = None
|
|
st.mode = None
|
|
await self._set_channel_status(player)
|
|
|
|
# ── track start → update voice status & announce ─────────────────────────
|
|
@commands.Cog.listener()
|
|
async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload):
|
|
player: wavelink.Player = payload.player
|
|
if player is None:
|
|
return
|
|
st = self.state(player.guild.id)
|
|
st.current = payload.track
|
|
|
|
if st.mode != MODE_RADIO:
|
|
await self._set_channel_status(player, st.current)
|
|
|
|
# Send polished now-playing announcement to the music text channel
|
|
if not st.suppress_np:
|
|
await self._send_now_playing(player.guild.id, st.current)
|
|
st.suppress_np = False
|
|
|
|
# ── now-playing announcement helper ───────────────────────────────────────
|
|
async def _send_now_playing(self, guild_id: int, track: wavelink.Playable):
|
|
"""Send a compact, polished 'Now Playing' embed to the music channel."""
|
|
channel_id = self._music_channels.get(guild_id)
|
|
if channel_id is None:
|
|
return
|
|
guild = self.bot.get_guild(guild_id)
|
|
if guild is None:
|
|
return
|
|
channel = guild.get_channel_or_thread(channel_id)
|
|
if channel is None:
|
|
self._music_channels.pop(guild_id, None)
|
|
return
|
|
|
|
# Build a clean now-playing embed
|
|
embed = discord.Embed(color=C_MAIN)
|
|
desc_parts = [f"## 🎵 {track.title}"]
|
|
if track.author:
|
|
desc_parts.append(f"-# {track.author}")
|
|
embed.description = "\n".join(desc_parts)
|
|
|
|
meta: list[str] = []
|
|
if track.length:
|
|
meta.append(f"`{fmt_duration(track.length)}`")
|
|
if track.extras and isinstance(track.extras, dict):
|
|
requester_id = track.extras.get("requester_id")
|
|
if requester_id:
|
|
meta.append(f"Requested by <@{requester_id}>")
|
|
if meta:
|
|
embed.add_field(name="\u200b", value=" · ".join(meta), inline=False)
|
|
|
|
if track.artwork:
|
|
embed.set_thumbnail(url=track.artwork)
|
|
embed.set_footer(text="yuuka music")
|
|
|
|
try:
|
|
await channel.send(embed=embed)
|
|
except Exception:
|
|
log.warning(f"Failed to send now-playing message in guild {guild_id}", exc_info=True)
|