diff --git a/tests/test_music_player.py b/tests/test_music_player.py index 5bf6fb0..13b4919 100644 --- a/tests/test_music_player.py +++ b/tests/test_music_player.py @@ -39,6 +39,15 @@ class MusicPlayerQueueTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(track.plays_left, 0) + async def test_skip_does_not_decrement_current_track(self): + player, _, state = await self._queue_abc(default_plays=2) + + player.skip(GUILD) + await player.play_next(GUILD, advance=True, consume_current=False) + + self.assertEqual(state.queue[0].plays_left, 2) + self.assertEqual(state.current.id, "b") + async def test_last_play_remains_pointed_to_until_playback_finishes(self): player, _ = build_player(default_plays=1) track = FakeTrack("a") diff --git a/zyrarbot/music/music_player.py b/zyrarbot/music/music_player.py index 7fd36e0..eb169ae 100644 --- a/zyrarbot/music/music_player.py +++ b/zyrarbot/music/music_player.py @@ -50,6 +50,7 @@ class MusicPlayer: self._queues = queues self._states: dict[int, GuildMusicState] = {} self._playback_tokens: dict[int, int] = {} + self._interrupted_playbacks: set[int] = set() self.on_state_change: StateChangeHook | None = None # --- Stan --------------------------------------------------------------- @@ -94,6 +95,7 @@ class MusicPlayer: def clear(self, guild_id: int) -> None: self._playback_tokens[guild_id] = self._playback_tokens.get(guild_id, 0) + 1 + self._interrupted_playbacks.discard(guild_id) self.state(guild_id).reset() def has_saved_queue(self, guild_id: int) -> bool: @@ -121,6 +123,13 @@ class MusicPlayer: if current is state.current: current.plays_left = max(self.plays_left(current, guild_id) - 1, 0) + def skip(self, guild_id: int) -> None: + """Stop the current source without consuming a play.""" + voice_client = self.voice_client(guild_id) + if voice_client and (voice_client.is_playing() or voice_client.is_paused()): + self._interrupted_playbacks.add(guild_id) + voice_client.stop() + async def restore_queue(self, guild_id: int) -> int: if not self._queues: return 0 @@ -128,6 +137,7 @@ class MusicPlayer: state = self.state(guild_id) playback_token = self._playback_tokens.get(guild_id, 0) + 1 self._playback_tokens[guild_id] = playback_token + self._interrupted_playbacks.discard(guild_id) voice_client = self.voice_client(guild_id) if voice_client and (voice_client.is_playing() or voice_client.is_paused()): voice_client.stop() @@ -272,13 +282,20 @@ class MusicPlayer: async def _restart_playback(self, guild_id: int) -> None: voice_client = self.voice_client(guild_id) if voice_client and (voice_client.is_playing() or voice_client.is_paused()): + self._interrupted_playbacks.add(guild_id) voice_client.stop() # `after` wywoła play_next, flaga przeskoku zablokuje advance else: await self.play_next(guild_id, advance=False) # --- Odtwarzanie -------------------------------------------------------- - async def play_next(self, guild_id: int, advance: bool = True, _failures: int = 0) -> None: + async def play_next( + self, + guild_id: int, + advance: bool = True, + _failures: int = 0, + consume_current: bool = True, + ) -> None: """Przechodzi do kolejnego utworu; ``_failures`` chroni przed pętlą pomijania.""" state = self.state(guild_id) playback_token = self._playback_tokens.get(guild_id, 0) @@ -290,7 +307,7 @@ class MusicPlayer: if state.consume_jump(): advance = False - if advance: + if advance and consume_current: self.finish_current(guild_id) # Utwór z ostatnim odtworzeniem musi pozostać w kolejce do chwili @@ -390,9 +407,13 @@ class MusicPlayer: def _schedule_next(self, guild_id: int, error: Exception | None, playback_token: int) -> None: if self._playback_tokens.get(guild_id) != playback_token: return + consume_current = guild_id not in self._interrupted_playbacks + self._interrupted_playbacks.discard(guild_id) if error: logger.error("Odtwarzanie zakończone błędem na serwerze %s: %s", guild_id, error) - asyncio.run_coroutine_threadsafe(self.play_next(guild_id, advance=True), self._bot.loop) + asyncio.run_coroutine_threadsafe( + self.play_next(guild_id, advance=True, consume_current=consume_current), self._bot.loop + ) def _record_history(self, guild_id: int, track: YTDLSource) -> None: if not track.title: diff --git a/zyrarbot/ui/api/routers/playback_router.py b/zyrarbot/ui/api/routers/playback_router.py index ba2c053..4a6b94a 100644 --- a/zyrarbot/ui/api/routers/playback_router.py +++ b/zyrarbot/ui/api/routers/playback_router.py @@ -122,7 +122,7 @@ async def skip( if not player.is_active(guild.id): raise HTTPException(status_code=400, detail="Bot is not playing anything on this server") - player.voice_client(guild.id).stop() + player.skip(guild.id) await broadcaster.broadcast() return {"status": "skipped"} diff --git a/zyrarbot/ui/bot/cogs/control_cog.py b/zyrarbot/ui/bot/cogs/control_cog.py index 5dfaaf0..7c56d69 100644 --- a/zyrarbot/ui/bot/cogs/control_cog.py +++ b/zyrarbot/ui/bot/cogs/control_cog.py @@ -19,7 +19,7 @@ class ControlCog(MusicCogBase, name="Control"): if not self.player.is_active(guild_id): await interaction.response.send_message("Nic nie jest odtwarzane.", ephemeral=True) return - interaction.guild.voice_client.stop() + self.player.skip(guild_id) await self.player.trigger_state_change() await interaction.response.send_message("Pominięto aktualny utwór.")