chore: snapshot 2026-02-08
This commit is contained in:
@@ -23,7 +23,7 @@ A discord music bot that seamlessly streams music from your personal music serve
|
|||||||
| `/queue` | View the current queue |
|
| `/queue` | View the current queue |
|
||||||
| `/clear` | Clear the current queue |
|
| `/clear` | Clear the current queue |
|
||||||
| `/shuffle` | Shuffles 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 |
|
| `/stop` | Stop playing the current track |
|
||||||
| `/autoplay` | Toggle autoplay |
|
| `/autoplay` | Toggle autoplay |
|
||||||
| `/playlists` | List available playlists |
|
| `/playlists` | List available playlists |
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ class DiscodromeClient(commands.Bot):
|
|||||||
async def on_ready(self) -> None:
|
async def on_ready(self) -> None:
|
||||||
''' Event called when the client is done preparing. '''
|
''' Event called when the client is done preparing. '''
|
||||||
|
|
||||||
activity = discord.Activity(type=discord.ActivityType.playing, name=env.BOT_STATUS)
|
await self.set_default_presence()
|
||||||
await self.change_presence(activity=activity)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Logged as: %s | Connected Guilds: %s | Loaded Extensions: %s",
|
"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)
|
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__":
|
if __name__ == "__main__":
|
||||||
logs.setup_logging()
|
logs.setup_logging()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -209,6 +209,12 @@ class MusicCog(commands.Cog):
|
|||||||
# Add current song back to the queue if exists
|
# Add current song back to the queue if exists
|
||||||
player.queue.insert(0, player.current_song)
|
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
|
# Display disconnect confirmation
|
||||||
await ui.SysMsg.stopping_queue_playback(interaction)
|
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}")
|
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")
|
@app_commands.command(name="skip", description="Skip one or more tracks")
|
||||||
async def skip(self, interaction: discord.Interaction) -> None:
|
@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 '''
|
''' Skip the current track '''
|
||||||
|
|
||||||
# Get the voice client instance
|
# Get the voice client instance
|
||||||
@@ -281,12 +288,17 @@ class MusicCog(commands.Cog):
|
|||||||
await ui.ErrMsg.bot_not_in_voice_channel(interaction)
|
await ui.ErrMsg.bot_not_in_voice_channel(interaction)
|
||||||
return
|
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
|
# Check if the bot is playing music
|
||||||
if not voice_client.is_playing():
|
if not voice_client.is_playing():
|
||||||
await ui.ErrMsg.not_playing(interaction)
|
await ui.ErrMsg.not_playing(interaction)
|
||||||
return
|
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
|
@skip.error
|
||||||
async def skip_error(self, ctx, 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 = data.guild_data(member.guild.id).player
|
||||||
player.queue.clear()
|
player.queue.clear()
|
||||||
player.current_song = None
|
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.")
|
logger.info("The bot has disconnected and cleared the queue as there are no users in the voice channel.")
|
||||||
else:
|
else:
|
||||||
logger.debug("Bot is no longer alone in voice channel, aborting disconnect...")
|
logger.debug("Bot is no longer alone in voice channel, aborting disconnect...")
|
||||||
|
|||||||
36
player.py
36
player.py
@@ -8,7 +8,7 @@ import data
|
|||||||
import ui
|
import ui
|
||||||
import logging
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -263,6 +263,12 @@ class Player():
|
|||||||
song = self.queue.pop(0)
|
song = self.queue.pop(0)
|
||||||
self.current_song = song
|
self.current_song = song
|
||||||
await ui.SysMsg.now_playing(interaction, 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)
|
await self.stream_track(interaction, song, voice_client)
|
||||||
else:
|
else:
|
||||||
logger.debug("Queue is empty.")
|
logger.debug("Queue is empty.")
|
||||||
@@ -278,10 +284,15 @@ class Player():
|
|||||||
return
|
return
|
||||||
# If the queue is empty, playback has ended; we should let the user know
|
# If the queue is empty, playback has ended; we should let the user know
|
||||||
await ui.SysMsg.playback_ended(interaction)
|
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:
|
async def skip_track(self, interaction: discord.Interaction, voice_client: discord.VoiceClient, count: int=None) -> None:
|
||||||
''' Skips the current track and plays the next one in the queue '''
|
''' 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
|
# 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:
|
if voice_client is None:
|
||||||
@@ -290,8 +301,25 @@ class Player():
|
|||||||
logger.debug("Skipping track...")
|
logger.debug("Skipping track...")
|
||||||
# Check if the bot is already playing something
|
# Check if the bot is already playing something
|
||||||
if voice_client.is_playing():
|
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()
|
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:
|
else:
|
||||||
await ui.ErrMsg.not_playing(interaction)
|
await ui.ErrMsg.not_playing(interaction)
|
||||||
|
|
||||||
|
|||||||
13
subsonic.py
13
subsonic.py
@@ -449,6 +449,19 @@ async def get_artist_discography(query: str) -> Album:
|
|||||||
return album_list
|
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:
|
async def get_album_art_file(cover_id: str, size: int=300) -> str:
|
||||||
''' Request album art from the subsonic API '''
|
''' Request album art from the subsonic API '''
|
||||||
target_path = f"cache/{cover_id}.jpg"
|
target_path = f"cache/{cover_id}.jpg"
|
||||||
|
|||||||
Reference in New Issue
Block a user