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
|
draggedVolume.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePlayback = () =>
|
async function togglePlayback() {
|
||||||
playing.value
|
if (!current.value) {
|
||||||
? run(() => api.pause(), 'Wstrzymano odtwarzanie', 'Błąd sterowania odtwarzaniem')
|
notify('Nic nie jest obecnie odtwarzane', 'info')
|
||||||
: run(() => api.resume(), 'Wznowiono odtwarzanie', 'Błąd sterowania odtwarzaniem')
|
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')
|
const skip = () => run(() => api.skip(), 'Pominięto utwór', 'Błąd pomijania utworu')
|
||||||
|
|
||||||
@@ -185,6 +192,7 @@ async function cycleLoop() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-icon btn-primary"
|
class="btn-icon btn-primary"
|
||||||
|
:disabled="!current"
|
||||||
:title="playing ? 'Wstrzymaj' : 'Odtwarzaj'"
|
:title="playing ? 'Wstrzymaj' : 'Odtwarzaj'"
|
||||||
@click="togglePlayback"
|
@click="togglePlayback"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ class MusicPlayerQueueTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(state.index, 1)
|
self.assertEqual(state.index, 1)
|
||||||
self.assertEqual(state.current.id, "b")
|
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):
|
async def test_loop_track_replays_same_index(self):
|
||||||
player, _, state = await self._queue_abc()
|
player, _, state = await self._queue_abc()
|
||||||
await player.play_next(GUILD, advance=True)
|
await player.play_next(GUILD, advance=True)
|
||||||
@@ -118,11 +128,24 @@ class MusicPlayerQueueTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertIsNone(state.current)
|
self.assertIsNone(state.current)
|
||||||
self.assertEqual(state.index, 3)
|
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):
|
async def test_jump_after_queue_end_plays_selected_track(self):
|
||||||
player, voice_client, state = await self._queue_abc()
|
player, voice_client, state = await self._queue_abc()
|
||||||
state.index = 2
|
state.index = 2
|
||||||
await player.play_next(GUILD, advance=True)
|
await player.play_next(GUILD, advance=True)
|
||||||
voice_client.playing = None
|
voice_client.playing = state.queue[2]
|
||||||
|
|
||||||
await player.jump(GUILD, 0)
|
await player.jump(GUILD, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,13 @@ class MusicPlayer:
|
|||||||
return discord.utils.get(self._bot.voice_clients, guild__id=guild_id)
|
return discord.utils.get(self._bot.voice_clients, guild__id=guild_id)
|
||||||
|
|
||||||
def is_active(self, guild_id: int) -> bool:
|
def is_active(self, guild_id: int) -> bool:
|
||||||
|
state = self.state(guild_id)
|
||||||
voice_client = self.voice_client(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:
|
def clear(self, guild_id: int) -> None:
|
||||||
self._playback_tokens[guild_id] = self._playback_tokens.get(guild_id, 0) + 1
|
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)
|
self._interrupted_playbacks.add(guild_id)
|
||||||
voice_client.stop()
|
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:
|
async def restore_queue(self, guild_id: int) -> int:
|
||||||
if not self._queues:
|
if not self._queues:
|
||||||
return 0
|
return 0
|
||||||
@@ -281,7 +294,18 @@ class MusicPlayer:
|
|||||||
|
|
||||||
async def _restart_playback(self, guild_id: int) -> None:
|
async def _restart_playback(self, guild_id: int) -> None:
|
||||||
voice_client = self.voice_client(guild_id)
|
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)
|
self._interrupted_playbacks.add(guild_id)
|
||||||
voice_client.stop() # `after` wywoła play_next, flaga przeskoku zablokuje advance
|
voice_client.stop() # `after` wywoła play_next, flaga przeskoku zablokuje advance
|
||||||
else:
|
else:
|
||||||
@@ -325,6 +349,7 @@ class MusicPlayer:
|
|||||||
if not state.queue:
|
if not state.queue:
|
||||||
state.index = -1
|
state.index = -1
|
||||||
state.current = None
|
state.current = None
|
||||||
|
self._stop_stale_voice(guild_id, voice_client)
|
||||||
await self.trigger_state_change()
|
await self.trigger_state_change()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -337,6 +362,7 @@ class MusicPlayer:
|
|||||||
if next_index >= len(state.queue):
|
if next_index >= len(state.queue):
|
||||||
state.index = next_index
|
state.index = next_index
|
||||||
state.current = None
|
state.current = None
|
||||||
|
self._stop_stale_voice(guild_id, voice_client)
|
||||||
await self.trigger_state_change()
|
await self.trigger_state_change()
|
||||||
await self.notify(guild_id, "Kolejka zakończona.")
|
await self.notify(guild_id, "Kolejka zakończona.")
|
||||||
return
|
return
|
||||||
@@ -405,15 +431,30 @@ class MusicPlayer:
|
|||||||
return index + 1
|
return index + 1
|
||||||
|
|
||||||
def _schedule_next(self, guild_id: int, error: Exception | None, playback_token: int) -> None:
|
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
|
return
|
||||||
consume_current = guild_id not in self._interrupted_playbacks
|
consume_current = guild_id not in self._interrupted_playbacks
|
||||||
self._interrupted_playbacks.discard(guild_id)
|
self._interrupted_playbacks.discard(guild_id)
|
||||||
if error:
|
if error:
|
||||||
logger.error("Odtwarzanie zakończone błędem na serwerze %s: %s", guild_id, 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
|
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:
|
def _record_history(self, guild_id: int, track: YTDLSource) -> None:
|
||||||
if not track.title:
|
if not track.title:
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class StatusService:
|
|||||||
notification_channel = self._player.notification_channel(guild.id)
|
notification_channel = self._player.notification_channel(guild.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"playing": bool(voice_client and voice_client.is_playing()),
|
"playing": bool(self._player.is_active(guild.id) and voice_client.is_playing()),
|
||||||
"paused": bool(voice_client and voice_client.is_paused()),
|
"paused": bool(self._player.is_active(guild.id) and voice_client.is_paused()),
|
||||||
"loop_single": state.loop_mode.loops_track,
|
"loop_single": state.loop_mode.loops_track,
|
||||||
"loop_playlist": state.loop_mode.loops_queue,
|
"loop_playlist": state.loop_mode.loops_queue,
|
||||||
"volume": int(state.volume * 100),
|
"volume": int(state.volume * 100),
|
||||||
|
|||||||
Reference in new issue
Block a user