fix: advance playback after track completion
This commit is contained in:
4 files changed
+83
-11
No files matched your search
@@ -80,10 +80,17 @@ async function commitVolume() {
|
||||
draggedVolume.value = null
|
||||
}
|
||||
|
||||
const togglePlayback = () =>
|
||||
playing.value
|
||||
? run(() => api.pause(), 'Wstrzymano odtwarzanie', 'Błąd sterowania odtwarzaniem')
|
||||
: run(() => api.resume(), 'Wznowiono odtwarzanie', 'Błąd sterowania odtwarzaniem')
|
||||
async function togglePlayback() {
|
||||
if (!current.value) {
|
||||
notify('Nic nie jest obecnie odtwarzane', 'info')
|
||||
return
|
||||
}
|
||||
await run(
|
||||
() => (playing.value ? api.pause() : api.resume()),
|
||||
playing.value ? 'Wstrzymano odtwarzanie' : 'Wznowiono odtwarzanie',
|
||||
'Błąd sterowania odtwarzaniem',
|
||||
)
|
||||
}
|
||||
|
||||
const skip = () => run(() => api.skip(), 'Pominięto utwór', 'Błąd pomijania utworu')
|
||||
|
||||
@@ -185,6 +192,7 @@ async function cycleLoop() {
|
||||
</button>
|
||||
<button
|
||||
class="btn-icon btn-primary"
|
||||
:disabled="!current"
|
||||
:title="playing ? 'Wstrzymaj' : 'Odtwarzaj'"
|
||||
@click="togglePlayback"
|
||||
>
|
||||
|
||||
@@ -93,6 +93,16 @@ class MusicPlayerQueueTest(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(state.index, 1)
|
||||
self.assertEqual(state.current.id, "b")
|
||||
|
||||
async def test_initial_playback_callback_advances_from_zero_token(self):
|
||||
_, voice_client, state = await self._queue_abc()
|
||||
|
||||
voice_client.after(None)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
self.assertEqual(state.index, 1)
|
||||
self.assertEqual(state.current.id, "b")
|
||||
self.assertEqual(state.queue[0].plays_left, 4)
|
||||
|
||||
async def test_loop_track_replays_same_index(self):
|
||||
player, _, state = await self._queue_abc()
|
||||
await player.play_next(GUILD, advance=True)
|
||||
@@ -118,11 +128,24 @@ class MusicPlayerQueueTest(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNone(state.current)
|
||||
self.assertEqual(state.index, 3)
|
||||
|
||||
async def test_queue_end_is_not_active_even_if_voice_client_lags(self):
|
||||
player, voice_client, state = await self._queue_abc()
|
||||
state.index = 2
|
||||
await player.play_next(GUILD, advance=True)
|
||||
|
||||
self.assertIsNone(state.current)
|
||||
self.assertFalse(voice_client.is_playing())
|
||||
self.assertFalse(player.is_active(GUILD))
|
||||
|
||||
await player.enqueue(GUILD, FakeTrack("new"))
|
||||
|
||||
self.assertEqual(state.current.id, "new")
|
||||
|
||||
async def test_jump_after_queue_end_plays_selected_track(self):
|
||||
player, voice_client, state = await self._queue_abc()
|
||||
state.index = 2
|
||||
await player.play_next(GUILD, advance=True)
|
||||
voice_client.playing = None
|
||||
voice_client.playing = state.queue[2]
|
||||
|
||||
await player.jump(GUILD, 0)
|
||||
|
||||
|
||||
@@ -90,8 +90,13 @@ class MusicPlayer:
|
||||
return discord.utils.get(self._bot.voice_clients, guild__id=guild_id)
|
||||
|
||||
def is_active(self, guild_id: int) -> bool:
|
||||
state = self.state(guild_id)
|
||||
voice_client = self.voice_client(guild_id)
|
||||
return bool(voice_client and (voice_client.is_playing() or voice_client.is_paused()))
|
||||
return bool(
|
||||
state.current
|
||||
and voice_client
|
||||
and (voice_client.is_playing() or voice_client.is_paused())
|
||||
)
|
||||
|
||||
def clear(self, guild_id: int) -> None:
|
||||
self._playback_tokens[guild_id] = self._playback_tokens.get(guild_id, 0) + 1
|
||||
@@ -130,6 +135,14 @@ class MusicPlayer:
|
||||
self._interrupted_playbacks.add(guild_id)
|
||||
voice_client.stop()
|
||||
|
||||
def _stop_stale_voice(self, guild_id: int, voice_client: discord.VoiceClient) -> None:
|
||||
"""Stop a voice client that still reports the source from a finished queue."""
|
||||
if not (voice_client.is_playing() or voice_client.is_paused()):
|
||||
return
|
||||
self._playback_tokens[guild_id] = self._playback_tokens.get(guild_id, 0) + 1
|
||||
self._interrupted_playbacks.discard(guild_id)
|
||||
voice_client.stop()
|
||||
|
||||
async def restore_queue(self, guild_id: int) -> int:
|
||||
if not self._queues:
|
||||
return 0
|
||||
@@ -281,7 +294,18 @@ 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()):
|
||||
voice_is_active = bool(
|
||||
voice_client and (voice_client.is_playing() or voice_client.is_paused())
|
||||
)
|
||||
if voice_is_active and not self.is_active(guild_id):
|
||||
# Po końcu kolejki discord.py może jeszcze przez chwilę trzymać stare źródło.
|
||||
# Unieważniamy jego callback i uruchamiamy wybrany utwór bez czekania na niego.
|
||||
self._playback_tokens[guild_id] = self._playback_tokens.get(guild_id, 0) + 1
|
||||
self._interrupted_playbacks.discard(guild_id)
|
||||
voice_client.stop()
|
||||
await self.play_next(guild_id, advance=False)
|
||||
return
|
||||
if self.is_active(guild_id):
|
||||
self._interrupted_playbacks.add(guild_id)
|
||||
voice_client.stop() # `after` wywoła play_next, flaga przeskoku zablokuje advance
|
||||
else:
|
||||
@@ -325,6 +349,7 @@ class MusicPlayer:
|
||||
if not state.queue:
|
||||
state.index = -1
|
||||
state.current = None
|
||||
self._stop_stale_voice(guild_id, voice_client)
|
||||
await self.trigger_state_change()
|
||||
return
|
||||
|
||||
@@ -337,6 +362,7 @@ class MusicPlayer:
|
||||
if next_index >= len(state.queue):
|
||||
state.index = next_index
|
||||
state.current = None
|
||||
self._stop_stale_voice(guild_id, voice_client)
|
||||
await self.trigger_state_change()
|
||||
await self.notify(guild_id, "Kolejka zakończona.")
|
||||
return
|
||||
@@ -405,15 +431,30 @@ class MusicPlayer:
|
||||
return index + 1
|
||||
|
||||
def _schedule_next(self, guild_id: int, error: Exception | None, playback_token: int) -> None:
|
||||
if self._playback_tokens.get(guild_id) != playback_token:
|
||||
logger.info(
|
||||
"Zakończono odtwarzanie na serwerze %s (token=%s, error=%s)",
|
||||
guild_id,
|
||||
playback_token,
|
||||
error,
|
||||
)
|
||||
if self._playback_tokens.get(guild_id, 0) != playback_token:
|
||||
logger.info("Pomijam nieaktualny callback odtwarzania na serwerze %s", guild_id)
|
||||
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(
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.play_next(guild_id, advance=True, consume_current=consume_current), self._bot.loop
|
||||
)
|
||||
future.add_done_callback(self._log_playback_failure)
|
||||
|
||||
@staticmethod
|
||||
def _log_playback_failure(future: asyncio.Future) -> None:
|
||||
try:
|
||||
future.result()
|
||||
except Exception:
|
||||
logger.exception("Nie udało się przejść do kolejnego utworu")
|
||||
|
||||
def _record_history(self, guild_id: int, track: YTDLSource) -> None:
|
||||
if not track.title:
|
||||
|
||||
@@ -65,8 +65,8 @@ class StatusService:
|
||||
notification_channel = self._player.notification_channel(guild.id)
|
||||
|
||||
return {
|
||||
"playing": bool(voice_client and voice_client.is_playing()),
|
||||
"paused": bool(voice_client and voice_client.is_paused()),
|
||||
"playing": bool(self._player.is_active(guild.id) and voice_client.is_playing()),
|
||||
"paused": bool(self._player.is_active(guild.id) and voice_client.is_paused()),
|
||||
"loop_single": state.loop_mode.loops_track,
|
||||
"loop_playlist": state.loop_mode.loops_queue,
|
||||
"volume": int(state.volume * 100),
|
||||
|
||||
Reference in new issue
Block a user