import wavelink import discord import logging from discord.ext import commands from ..shared.constants import LOOP_TRACK, LOOP_QUEUE, MODE_RADIO 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) 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 ───────────────────────────────────── @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 # Radio sets its own title; only update status for music mode if st.mode != MODE_RADIO: await self._set_channel_status(player, st.current)