diff --git a/cogs/player/jockey.py b/cogs/player/jockey.py index b2950e4..7a77b2d 100644 --- a/cogs/player/jockey.py +++ b/cogs/player/jockey.py @@ -10,6 +10,7 @@ from nextcord import (Colour, Forbidden, HTTPException, Message, NotFound, StageChannel, VoiceChannel) +from database.redis import REDIS from dataclass.custom_embed import CustomEmbed from utils.embeds import create_error_embed from utils.exceptions import (EndOfQueueError, JockeyError, JockeyException, @@ -18,7 +19,8 @@ from utils.time import human_readable_time from views.now_playing import NowPlayingView -from .jockey_helpers import find_lavalink_track, parse_query +from .jockey_helpers import (find_lavalink_track, invalidate_lavalink_track, + parse_query) from .queue import QueueManager if TYPE_CHECKING: @@ -196,7 +198,24 @@ async def _play(self, item: 'QueueItem') -> bool: raise RuntimeError(err.args[0]) from err # Play track - await self.play(item.lavalink_track, volume=self.volume) + has_retried = False + while True: + try: + await self.play(item.lavalink_track, volume=self.volume) + except PlayerNotConnected: + # If we've already retried, give up + if has_retried or REDIS is None: + raise + + # Remove cached Lavalink track and try again + self._logger.warning( + 'PlayerNotConnected raised while playing `%s\'. Retrying.', + item.title + ) + invalidate_lavalink_track(item) + has_retried = True + else: + break # We don't want to play if the player is not idle # as that will effectively skip the current track. diff --git a/cogs/player/jockey_helpers.py b/cogs/player/jockey_helpers.py index 77dfa3d..3bebf34 100644 --- a/cogs/player/jockey_helpers.py +++ b/cogs/player/jockey_helpers.py @@ -260,6 +260,38 @@ async def find_lavalink_track( # pylint: disable=too-many-statements return lavalink_track +def invalidate_lavalink_track(item: QueueItem): + """ + Removes a cached Lavalink track from Redis. + + :param item: The QueueItem to invalidate the track for. + """ + if REDIS is None: + return + + # Determine key type + redis_key = None + redis_key_type = None + if item.spotify_id is not None: + redis_key = item.spotify_id + redis_key_type = 'spotify_id' + elif item.isrc is not None: + redis_key = item.isrc + redis_key_type = 'isrc' + + # Invalidate cached Lavalink track + if redis_key is not None and redis_key_type is not None: + REDIS.invalidate_lavalink_track( + redis_key, + key_type=redis_key_type + ) + else: + LOGGER.warning( + 'Could not invalidate cached track for `%s\': no key', + item.title + ) + + async def parse_query( node: 'Node', spotify: Spotify, diff --git a/database/redis.py b/database/redis.py index ed6c7f2..a8b2fd0 100644 --- a/database/redis.py +++ b/database/redis.py @@ -61,6 +61,17 @@ def get_lavalink_track(self, key: str, *, key_type: str) -> Optional[str]: self._logger.debug('Got cached Lavalink track for %s:%s', key_type, key) return self._client.get(f'lavalink:{key_type}:{key}') # type: ignore + def invalidate_lavalink_track(self, key: str, *, key_type: str): + """ + Removes a cached Lavalink track. + + :param key: The key to remove the track for. + :param key_type: The type of key to remove the track for, e.g. 'isrc' or 'spotify_id'. + """ + self._logger.debug('Invalidating Lavalink track for %s:%s', key_type, key) + if self._client.exists(f'lavalink:{key_type}:{key}'): + self._client.delete(f'lavalink:{key_type}:{key}') + def set_spotify_track(self, spotify_id: str, track: 'SpotifyTrack'): """ Save a Spotify track.