Fix radio race conditions, add mode re-check, add stream-drop notifications
This commit is contained in:
1 parent
83383e74e0
commit
19b51f16aa
1 file changed
+79
-12
+79
-12
@@ -128,7 +128,6 @@ class _StationSelect(discord.ui.View):
|
|||||||
self._interaction = interaction
|
self._interaction = interaction
|
||||||
self.message: discord.Message | None = None
|
self.message: discord.Message | None = None
|
||||||
self._picked: dict | None = None
|
self._picked: dict | None = None
|
||||||
self._done = asyncio.Event()
|
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
for s in stations:
|
for s in stations:
|
||||||
@@ -158,7 +157,6 @@ class _StationSelect(discord.ui.View):
|
|||||||
cog = self.bot.get_cog("Music")
|
cog = self.bot.get_cog("Music")
|
||||||
if cog and self._picked:
|
if cog and self._picked:
|
||||||
await cog._play_radio_stream(interaction, self.category, self._picked)
|
await cog._play_radio_stream(interaction, self.category, self._picked)
|
||||||
self._done.set()
|
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
async def on_timeout(self):
|
async def on_timeout(self):
|
||||||
@@ -177,6 +175,7 @@ class Music(commands.Cog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self._states: dict[int, GuildState] = {}
|
self._states: dict[int, GuildState] = {}
|
||||||
self._voice_channels: dict[int, int] = {} # guild_id → channel_id for disconnect cleanup
|
self._voice_channels: dict[int, int] = {} # guild_id → channel_id for disconnect cleanup
|
||||||
|
self._radio_channels: dict[int, int] = {} # guild_id → channel_id for radio-drop notifications
|
||||||
self._locks: dict[int, asyncio.Lock] = {}
|
self._locks: dict[int, asyncio.Lock] = {}
|
||||||
|
|
||||||
# Load radio stations
|
# Load radio stations
|
||||||
@@ -260,6 +259,7 @@ class Music(commands.Cog):
|
|||||||
if st:
|
if st:
|
||||||
st.queue.clear()
|
st.queue.clear()
|
||||||
self._locks.pop(guild_id, None)
|
self._locks.pop(guild_id, None)
|
||||||
|
self._radio_channels.pop(guild_id, None)
|
||||||
await self._clear_channel_status(guild_id)
|
await self._clear_channel_status(guild_id)
|
||||||
|
|
||||||
# ── track end → advance queue ─────────────────────────────────────────────
|
# ── track end → advance queue ─────────────────────────────────────────────
|
||||||
@@ -271,12 +271,6 @@ class Music(commands.Cog):
|
|||||||
|
|
||||||
st = self.state(player.guild.id)
|
st = self.state(player.guild.id)
|
||||||
|
|
||||||
# Radio mode: stream ended — just stop
|
|
||||||
if st.mode == MODE_RADIO:
|
|
||||||
st.mode = None
|
|
||||||
await self._set_channel_status(player)
|
|
||||||
return
|
|
||||||
|
|
||||||
if st.loop == LOOP_TRACK and st.current:
|
if st.loop == LOOP_TRACK and st.current:
|
||||||
await player.play(st.current)
|
await player.play(st.current)
|
||||||
await self._set_channel_status(player, st.current)
|
await self._set_channel_status(player, st.current)
|
||||||
@@ -297,10 +291,57 @@ class Music(commands.Cog):
|
|||||||
# wavelink built-in autoplay
|
# wavelink built-in autoplay
|
||||||
await player.set_autoplay(wavelink.AutoPlayMode.enabled)
|
await player.set_autoplay(wavelink.AutoPlayMode.enabled)
|
||||||
else:
|
else:
|
||||||
|
# If a radio stream dropped unexpectedly, notify the channel
|
||||||
|
if st.mode == MODE_RADIO:
|
||||||
|
await self._notify_radio_drop(player.guild.id)
|
||||||
st.current = None
|
st.current = None
|
||||||
st.mode = None
|
st.mode = None
|
||||||
await self._set_channel_status(player)
|
await self._set_channel_status(player)
|
||||||
|
|
||||||
|
# ── track exception / stuck → radio stream failures ──────────────────────
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_wavelink_track_exception(self, payload: wavelink.TrackExceptionEventPayload):
|
||||||
|
player: wavelink.Player = payload.player
|
||||||
|
if player is None:
|
||||||
|
return
|
||||||
|
st = self.state(player.guild.id)
|
||||||
|
if st.mode == MODE_RADIO:
|
||||||
|
log.warning(f"Radio stream exception in guild {player.guild.id}: {payload.exception}")
|
||||||
|
await self._notify_radio_drop(player.guild.id)
|
||||||
|
st.mode = None
|
||||||
|
await self._set_channel_status(player)
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_wavelink_track_stuck(self, payload: wavelink.TrackStuckEventPayload):
|
||||||
|
player: wavelink.Player = payload.player
|
||||||
|
if player is None:
|
||||||
|
return
|
||||||
|
st = self.state(player.guild.id)
|
||||||
|
if st.mode == MODE_RADIO:
|
||||||
|
log.warning(f"Radio stream stuck in guild {player.guild.id}, threshold {payload.threshold_ms}ms")
|
||||||
|
await self._notify_radio_drop(player.guild.id)
|
||||||
|
st.mode = None
|
||||||
|
await self._set_channel_status(player)
|
||||||
|
|
||||||
|
async def _notify_radio_drop(self, guild_id: int):
|
||||||
|
"""Send a notification to the channel where radio was started."""
|
||||||
|
channel_id = self._radio_channels.pop(guild_id, None)
|
||||||
|
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:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await channel.send(embed=discord.Embed(
|
||||||
|
description="📻 Radio stream ended unexpectedly. Use `!radio` to restart.",
|
||||||
|
color=C_WARN,
|
||||||
|
))
|
||||||
|
except Exception:
|
||||||
|
log.warning(f"Failed to send radio-drop notification in guild {guild_id}", exc_info=True)
|
||||||
|
|
||||||
# ── track start → update status ───────────────────────────────────────────
|
# ── track start → update status ───────────────────────────────────────────
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload):
|
async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload):
|
||||||
@@ -457,6 +498,7 @@ class Music(commands.Cog):
|
|||||||
await vc.disconnect()
|
await vc.disconnect()
|
||||||
|
|
||||||
self._voice_channels.pop(ctx.guild.id, None)
|
self._voice_channels.pop(ctx.guild.id, None)
|
||||||
|
self._radio_channels.pop(ctx.guild.id, None)
|
||||||
self._locks.pop(ctx.guild.id, None)
|
self._locks.pop(ctx.guild.id, None)
|
||||||
|
|
||||||
if current_mode == MODE_RADIO:
|
if current_mode == MODE_RADIO:
|
||||||
@@ -706,6 +748,17 @@ class Music(commands.Cog):
|
|||||||
st = self.state(guild.id)
|
st = self.state(guild.id)
|
||||||
|
|
||||||
async with self._lock(guild.id):
|
async with self._lock(guild.id):
|
||||||
|
# Re-check mode — music may have started between !radio and now
|
||||||
|
if st.mode == MODE_MUSIC:
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
description="🎵 Music is currently playing. Use `!stop` first to switch to radio.",
|
||||||
|
color=C_WARN,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Connect or reuse existing player
|
# Connect or reuse existing player
|
||||||
vc = guild.voice_client
|
vc = guild.voice_client
|
||||||
if not isinstance(vc, wavelink.Player):
|
if not isinstance(vc, wavelink.Player):
|
||||||
@@ -721,8 +774,11 @@ class Music(commands.Cog):
|
|||||||
vc.inactive_timeout = None
|
vc.inactive_timeout = None
|
||||||
self._voice_channels[guild.id] = member.voice.channel.id
|
self._voice_channels[guild.id] = member.voice.channel.id
|
||||||
|
|
||||||
# Stop anything currently playing
|
# Clear mode before skipping so the old track_end handler
|
||||||
|
# doesn't interfere with the new station's state
|
||||||
if vc.playing or vc.paused:
|
if vc.playing or vc.paused:
|
||||||
|
st.mode = None
|
||||||
|
st.current = None
|
||||||
await vc.skip()
|
await vc.skip()
|
||||||
|
|
||||||
# Resolve and play the stream URL via Lavalink
|
# Resolve and play the stream URL via Lavalink
|
||||||
@@ -746,14 +802,25 @@ class Music(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await vc.play(track)
|
# Set radio state *before* play so on_wavelink_track_start
|
||||||
|
# sees MODE_RADIO and doesn't overwrite our status text
|
||||||
# Set mode and clear any music state
|
|
||||||
st.queue.clear()
|
st.queue.clear()
|
||||||
st.current = track
|
st.current = track
|
||||||
st.loop = LOOP_OFF
|
st.loop = LOOP_OFF
|
||||||
st.autoplay = False
|
st.autoplay = False
|
||||||
st.mode = MODE_RADIO
|
st.mode = MODE_RADIO
|
||||||
|
self._radio_channels[guild.id] = interaction.channel_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
await vc.play(track)
|
||||||
|
except Exception as e:
|
||||||
|
st.mode = None
|
||||||
|
self._radio_channels.pop(guild.id, None)
|
||||||
|
await interaction.followup.send(
|
||||||
|
embed=discord.Embed(description=f"❌ Failed to play stream: {e}", color=C_ERR),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Set channel status with the JSON station name
|
# Set channel status with the JSON station name
|
||||||
await self._set_channel_status(vc, title=station["name"])
|
await self._set_channel_status(vc, title=station["name"])
|
||||||
|
|||||||
Reference in new issue
Block a user