From 2fd0f622f76343e738af22de92fc3c3898147346 Mon Sep 17 00:00:00 2001 From: joelilas Date: Sun, 8 Feb 2026 17:04:57 +0100 Subject: [PATCH] chore: snapshot 2026-02-08 --- README.md | 2 +- discodrome.py | 19 +++++++++++++++++-- extensions/music.py | 23 ++++++++++++++++++++--- player.py | 36 ++++++++++++++++++++++++++++++++---- subsonic.py | 13 +++++++++++++ 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 261602c..d0cde97 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A discord music bot that seamlessly streams music from your personal music serve | `/queue` | View the current queue | | `/clear` | Clear the current queue | | `/shuffle` | Shuffles the current queue | -| `/skip` | Skip the current track | +| `/skip [count]` | Skip the current track or `count` tracks | | `/stop` | Stop playing the current track | | `/autoplay` | Toggle autoplay | | `/playlists` | List available playlists | diff --git a/discodrome.py b/discodrome.py index 1b3bd76..029047f 100644 --- a/discodrome.py +++ b/discodrome.py @@ -76,8 +76,7 @@ class DiscodromeClient(commands.Bot): async def on_ready(self) -> None: ''' Event called when the client is done preparing. ''' - activity = discord.Activity(type=discord.ActivityType.playing, name=env.BOT_STATUS) - await self.change_presence(activity=activity) + await self.set_default_presence() logger.info( "Logged as: %s | Connected Guilds: %s | Loaded Extensions: %s", @@ -87,6 +86,22 @@ class DiscodromeClient(commands.Bot): ) logger.info("Bot status set to: '%s'", env.BOT_STATUS) + async def set_default_presence(self) -> None: + ''' Sets the default bot presence (idle status). ''' + activity = discord.Activity(type=discord.ActivityType.playing, name=env.BOT_STATUS) + await self.change_presence(activity=activity) + + async def set_now_playing(self, song_title: str, artist: str, cover_url: str = None) -> None: + ''' Sets the bot presence to show the currently playing song. ''' + activity = discord.Activity( + type=discord.ActivityType.listening, + name=f"{song_title} - {artist}", + large_image=cover_url, + large_text=f"{song_title} - {artist}" + ) + await self.change_presence(activity=activity) + logger.debug("Bot presence updated: Listening to '%s - %s'", song_title, artist) + if __name__ == "__main__": logs.setup_logging() logger = logging.getLogger(__name__) diff --git a/extensions/music.py b/extensions/music.py index 8ac742c..ff07c07 100644 --- a/extensions/music.py +++ b/extensions/music.py @@ -209,6 +209,12 @@ class MusicCog(commands.Cog): # Add current song back to the queue if exists player.queue.insert(0, player.current_song) + # Reset bot presence to default + try: + await self.bot.set_default_presence() + except Exception as e: + logger.error(f"Failed to reset bot presence: {e}") + # Display disconnect confirmation await ui.SysMsg.stopping_queue_playback(interaction) @@ -269,8 +275,9 @@ class MusicCog(commands.Cog): await ui.ErrMsg.msg(ctx, f"An unknown error has occurred and has been logged to console. Please contact an administrator. {error}") - @app_commands.command(name="skip", description="Skip the current track") - async def skip(self, interaction: discord.Interaction) -> None: + @app_commands.command(name="skip", description="Skip one or more tracks") + @app_commands.describe(count="Optional number of tracks to skip (>= 1)") + async def skip(self, interaction: discord.Interaction, count: int=None) -> None: ''' Skip the current track ''' # Get the voice client instance @@ -281,12 +288,17 @@ class MusicCog(commands.Cog): await ui.ErrMsg.bot_not_in_voice_channel(interaction) return + # Validate count if provided + if count is not None and count < 1: + await ui.ErrMsg.msg(interaction, "Skip count must be at least 1.") + return + # Check if the bot is playing music if not voice_client.is_playing(): await ui.ErrMsg.not_playing(interaction) return - await data.guild_data(interaction.guild_id).player.skip_track(interaction, voice_client) + await data.guild_data(interaction.guild_id).player.skip_track(interaction, voice_client, count=count) @skip.error async def skip_error(self, ctx, error): @@ -463,6 +475,11 @@ class MusicCog(commands.Cog): player = data.guild_data(member.guild.id).player player.queue.clear() player.current_song = None + # Reset bot presence to default + try: + await self.bot.set_default_presence() + except Exception as e: + logger.error(f"Failed to reset bot presence: {e}") logger.info("The bot has disconnected and cleared the queue as there are no users in the voice channel.") else: logger.debug("Bot is no longer alone in voice channel, aborting disconnect...") diff --git a/player.py b/player.py index 7de2701..6a58498 100644 --- a/player.py +++ b/player.py @@ -8,7 +8,7 @@ import data import ui import logging -from subsonic import Song, APIError, get_random_songs, get_similar_songs, stream, scrobble +from subsonic import Song, APIError, get_random_songs, get_similar_songs, stream, scrobble, get_album_art_url logger = logging.getLogger(__name__) @@ -263,6 +263,12 @@ class Player(): song = self.queue.pop(0) self.current_song = song await ui.SysMsg.now_playing(interaction, song) + # Update bot presence with now playing info + try: + cover_url = get_album_art_url(song.cover_id) + await interaction.client.set_now_playing(song.title, song.artist, cover_url) + except Exception as e: + logger.error(f"Failed to update bot presence: {e}") await self.stream_track(interaction, song, voice_client) else: logger.debug("Queue is empty.") @@ -278,10 +284,15 @@ class Player(): return # If the queue is empty, playback has ended; we should let the user know await ui.SysMsg.playback_ended(interaction) + # Reset bot presence to default + try: + await interaction.client.set_default_presence() + except Exception as e: + logger.error(f"Failed to reset bot presence: {e}") - async def skip_track(self, interaction: discord.Interaction, voice_client: discord.VoiceClient) -> None: - ''' Skips the current track and plays the next one in the queue ''' + async def skip_track(self, interaction: discord.Interaction, voice_client: discord.VoiceClient, count: int=None) -> None: + ''' Skips the current track and optionally additional tracks from the queue ''' # Check if the bot is connected to a voice channel; it's the caller's responsibility to open a voice channel if voice_client is None: @@ -290,8 +301,25 @@ class Player(): logger.debug("Skipping track...") # Check if the bot is already playing something if voice_client.is_playing(): + # Determine how many tracks to skip in total (current + additional) + total_skips = 1 if count is None or count < 1 else count + + # Stop current playback (this counts as 1 skip) voice_client.stop() - await ui.SysMsg.skipping(interaction) + + # Remove additional tracks from the front of the queue, if any + additional_to_skip = max(0, total_skips - 1) + if additional_to_skip > 0 and self.queue: + removed = min(additional_to_skip, len(self.queue)) + # Slice off the skipped items + self.queue = self.queue[removed:] + logger.debug(f"Skipped {removed} additional track(s) from queue") + + # Notify user + if total_skips <= 1: + await ui.SysMsg.skipping(interaction) + else: + await ui.SysMsg.msg(interaction, f"Skipped {total_skips} track{'s' if total_skips != 1 else ''}", ephemeral=True) else: await ui.ErrMsg.not_playing(interaction) diff --git a/subsonic.py b/subsonic.py index 2ecd887..7f212d7 100644 --- a/subsonic.py +++ b/subsonic.py @@ -449,6 +449,19 @@ async def get_artist_discography(query: str) -> Album: return album_list +def get_album_art_url(cover_id: str, size: int=300) -> str: + ''' Get the URL for album art from the subsonic API ''' + if not cover_id: + return None + params = { + **SUBSONIC_REQUEST_PARAMS, + "id": cover_id, + "size": str(size) + } + query_string = "&".join(f"{k}={v}" for k, v in params.items()) + return f"{env.SUBSONIC_SERVER}/rest/getCoverArt?{query_string}" + + async def get_album_art_file(cover_id: str, size: int=300) -> str: ''' Request album art from the subsonic API ''' target_path = f"cache/{cover_id}.jpg"