From d344acf06330ec351a9476375943037815d4b6a5 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Wed, 10 Sep 2025 19:25:47 +0200 Subject: [PATCH 01/13] Changed queue to FILO --- src/disopy/cogs/queue.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index f91d39c..be6644b 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -94,7 +94,7 @@ def pop(self, interaction: Interaction) -> Song | None: if id is None: return None - return self.queue[id].pop() + return self.queue[id].popleft() def append(self, interaction: Interaction, song: Song) -> None: """Append new songs to the queue. @@ -276,8 +276,6 @@ async def play( song_name: The name of the song. """ - await interaction.response.defer(thinking=True) - # Extract the type of element to be search, taking care of the default value choice = what if isinstance(what, str) else what.value @@ -474,9 +472,6 @@ async def volume(self, interaction: Interaction, volume: int) -> None: volume: The new volume level. """ - # Defer immediately to avoid timeout - await interaction.response.defer(thinking=True) - voice_client = await self.get_voice_client(interaction) if voice_client is None: return From 3e9902fced3d343a98f1ba38c537e71e90231566 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Wed, 10 Sep 2025 19:28:27 +0200 Subject: [PATCH 02/13] fix queue after FILO --- src/disopy/cogs/queue.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index be6644b..b73e902 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -276,6 +276,8 @@ async def play( song_name: The name of the song. """ + await interaction.response.defer(thinking=True) + # Extract the type of element to be search, taking care of the default value choice = what if isinstance(what, str) else what.value @@ -472,6 +474,9 @@ async def volume(self, interaction: Interaction, volume: int) -> None: volume: The new volume level. """ + # Defer immediately to avoid timeout + await interaction.response.defer(thinking=True) + voice_client = await self.get_voice_client(interaction) if voice_client is None: return @@ -486,4 +491,4 @@ async def volume(self, interaction: Interaction, volume: int) -> None: # Every source has a volume handler attach to it so suppressing the mypy error is safe voice_client.source.volume = volume / 100 # type: ignore[attr-defined] - await self.send_answer(interaction, f"🔊 Volume level set to {volume}%") + await self.send_answer(interaction, f"🔊 Volume level set to {volume}%") \ No newline at end of file From afd1bb821bb437afbafc309b5d7e4b901d5cd23b Mon Sep 17 00:00:00 2001 From: leamikmik Date: Wed, 10 Sep 2025 21:46:13 +0200 Subject: [PATCH 03/13] Added shuffle option --- src/disopy/cogs/queue.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index b73e902..d6443ec 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -6,6 +6,7 @@ import logging from collections import deque +from random import sample from typing import Iterable, NamedTuple, cast import discord @@ -124,8 +125,20 @@ def length(self, interaction: Interaction) -> int: if id is None: # A little ugly but gets the job done return 0 - + return len(self.queue[id]) + + def shuffle(self, interaction: Interaction) -> None: + """Shuffles the currenc queue + + Args: + interaction: The interaction where the guild ID can be found. + + """ + id = self._check_guild(interaction) + if id is None: + return + self.queue[id] = deque(sample(self.queue[id],len(self.queue[id]))) class QueueCog(Base): @@ -439,6 +452,24 @@ async def resume(self, interaction: Interaction) -> None: self.play_queue(interaction, None) await self.send_answer(interaction, "▶️ Resuming the playback") + @app_commands.command(name="shuffle", description="Shuffles the current queue") + async def shuffle_command(self, interaction: Interaction) -> None: + """Mixes the songs in the queue, if one exists + + Args: + interaction: The interaction that started the command. + """ + voice_client = await self.get_voice_client(interaction) + if voice_client is None: + return + + if self.queue.length(interaction) == 0: + await self.send_error(interaction, ["The queue is empty"]) + return + + self.queue.shuffle(interaction) + await self.send_answer(interaction, "🔀 Shuffling the queue") + @app_commands.command(name="queue", description="See the current queue") # Name changed to avoid collisions with the property `queue` async def queue_command(self, interaction: Interaction) -> None: @@ -475,7 +506,7 @@ async def volume(self, interaction: Interaction, volume: int) -> None: """ # Defer immediately to avoid timeout - await interaction.response.defer(thinking=True) + # await interaction.response.defer(thinking=True) voice_client = await self.get_voice_client(interaction) if voice_client is None: From c3ed52b7daf0620ed6fb1c406fcace35fc517cb7 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 00:41:58 +0200 Subject: [PATCH 04/13] Improved queue and Song class --- src/disopy/cogs/queue.py | 59 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index d6443ec..d024032 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -7,7 +7,9 @@ import logging from collections import deque from random import sample +from math import ceil from typing import Iterable, NamedTuple, cast +from itertools import islice import discord from discord import PCMVolumeTransformer, VoiceClient, app_commands @@ -20,6 +22,10 @@ from ..options import Options from .base import Base +def convert(seconds): + min, sec = divmod(seconds, 60) + hour, min = divmod(min, 60) + return '%d:%02d:%02d' % (hour, min, sec) if hour > 0 else '%02d:%02d' % (min, sec) class Song(NamedTuple): """Data representation for a Subsonic song. @@ -27,11 +33,14 @@ class Song(NamedTuple): Attributes: id: The ID in the Subsonic server. title: The title of the song. + artist: The primary artist of the song + duration: The duration of the song in seconds """ id: str title: str - + artist: str + duration: int logger = logging.getLogger(__name__) @@ -129,7 +138,7 @@ def length(self, interaction: Interaction) -> int: return len(self.queue[id]) def shuffle(self, interaction: Interaction) -> None: - """Shuffles the currenc queue + """Shuffles the current queue Args: interaction: The interaction where the guild ID can be found. @@ -140,6 +149,21 @@ def shuffle(self, interaction: Interaction) -> None: return self.queue[id] = deque(sample(self.queue[id],len(self.queue[id]))) + def duration(self, interaction: Interaction) -> int: + """Calculates the remaining duration of the queue, without the current song + + Args: + interaction: The interaction where the guild ID can be found. + + Returns: + Seconds of remaining duration + """ + id = self._check_guild(interaction) + if id is None: + return 0 + + return sum(song.duration for song in self.queue[id]) + class QueueCog(Base): """Cog that holds queue handling and music playback commands.""" @@ -289,7 +313,7 @@ async def play( song_name: The name of the song. """ - await interaction.response.defer(thinking=True) + # await interaction.response.defer(thinking=True) # Extract the type of element to be search, taking care of the default value choice = what if isinstance(what, str) else what.value @@ -314,7 +338,7 @@ async def play( return playing_element_name = song.title - self.queue.append(interaction, Song(song.id, song.title)) + self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) case "album": albums = self.subsonic.searching.search(query, song_count=0, album_count=10, artist_count=0).albums @@ -336,7 +360,7 @@ async def play( logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") continue - self.queue.append(interaction, Song(song.id, song.title)) + self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) case "playlist": for playlist in self.subsonic.playlists.get_playlists(): @@ -357,7 +381,7 @@ async def play( logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") continue - self.queue.append(interaction, Song(song.id, song.title)) + self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) break if first_play: @@ -472,24 +496,31 @@ async def shuffle_command(self, interaction: Interaction) -> None: @app_commands.command(name="queue", description="See the current queue") # Name changed to avoid collisions with the property `queue` - async def queue_command(self, interaction: Interaction) -> None: + async def queue_command(self, interaction: Interaction, page: int = 1) -> None: """List the songs added to the queue. Args: interaction: The interaction that started the command. """ - content = [] + max_page = ceil(self.queue.length(interaction)/10) + length = self.queue.length(interaction) + + if 1 > page or page > max_page: + await self.send_error(interaction, ["Out of queue bounds"]) + if self.now_playing is not None: - content.append(f"Now playing: **{self.now_playing.title}**") + content.append(f"Now playing: {self.now_playing.artist} - **{self.now_playing.title}**") content.append("") - length = self.queue.length(interaction) - + page -= 1 if length > 0: - content.append("Next:") - for song in self.queue.get(interaction): - content.append(f"- **{song.title}**") + content.append(f"""Remaining time - {convert(self.queue.duration(interaction))} + Pages - {page+1}/{max_page} + + Next:""") + for num, song in enumerate(islice(self.queue.get(interaction), 10*page, 10*(page + 1))): + content.append(f"{10*page + num + 1}. {song.artist} - **{song.title}**\t[{convert(song.duration)}]") if length == 0: content.append("_Queue empty_") From e2f439b41f2421a9fe009a4c6de71423d9215e68 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 15:06:23 +0200 Subject: [PATCH 05/13] Improved search text --- src/disopy/cogs/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/disopy/cogs/search.py b/src/disopy/cogs/search.py index c6b5744..c6f1d6f 100644 --- a/src/disopy/cogs/search.py +++ b/src/disopy/cogs/search.py @@ -65,7 +65,7 @@ def api_search(self, query: str, choice: str) -> tuple[str, list[str]]: for song in songs: content.append( - f"- **{song.title}**" + (f" - {song.artist.name}" if song.artist is not None else "") + "- " + (f"{song.artist.name} - " if song.artist is not None else "") + f"**{song.title}** [{self.seconds_to_str(song.duration)}]" ) case "album": @@ -74,7 +74,7 @@ def api_search(self, query: str, choice: str) -> tuple[str, list[str]]: for album in albums: content.append( - f"- **{album.name}**" + (f" - {album.artist.name}" if album.artist is not None else "") + "- " + (f"{album.artist.name} - " if album.artist is not None else "") + f"**{album.name}**" ) case "artist": From 6e988a113e02e2ca97063c9177d0fa8472b06f48 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 15:27:20 +0200 Subject: [PATCH 06/13] Added function to convert time --- src/disopy/cogs/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/disopy/cogs/base.py b/src/disopy/cogs/base.py index 81560cb..2224a03 100644 --- a/src/disopy/cogs/base.py +++ b/src/disopy/cogs/base.py @@ -28,6 +28,16 @@ def __init__(self, bot: Bot, options: Options) -> None: self.bot = bot self.options = options + def seconds_to_str(self, seconds: int) -> str: + """Converts seconds to h:m:s + + Args: + seconds: Time in seconds + """ + min, sec = divmod(seconds, 60) + hour, min = divmod(min, 60) + return '%d:%02d:%02d' % (hour, min, sec) if hour > 0 else '%02d:%02d' % (min, sec) + async def send_answer( self, interaction: Interaction, title: str, content: list[str] | None = None, ephemeral: bool = False ) -> None: From d7088b1a1604f97df1b502356a0039b16b384aa4 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 16:36:03 +0200 Subject: [PATCH 07/13] Searching songs to play now done in autocomplete --- src/disopy/cogs/queue.py | 128 +++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 53 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index d024032..cf29cfb 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -22,11 +22,6 @@ from ..options import Options from .base import Base -def convert(seconds): - min, sec = divmod(seconds, 60) - hour, min = divmod(min, 60) - return '%d:%02d:%02d' % (hour, min, sec) if hour > 0 else '%02d:%02d' % (min, sec) - class Song(NamedTuple): """Data representation for a Subsonic song. @@ -276,7 +271,7 @@ def play_queue(self, interaction: Interaction, exception: Exception | None) -> N if interaction.guild.voice_client is None: logger.warning("There is not available voice client in this interaction!") return - + voice_client = cast(VoiceClient, interaction.guild.voice_client) voice_client.play( @@ -291,67 +286,73 @@ def play_queue(self, interaction: Interaction, exception: Exception | None) -> N self.now_playing = song voice_client.source = PCMVolumeTransformer(voice_client.source, volume=self.config.volume / 100) - @app_commands.command(description="Add a song, album, or playlist to the queue") - @app_commands.choices( - what=[ - app_commands.Choice(name="Song", value="song"), - app_commands.Choice(name="Album", value="album"), - app_commands.Choice(name="Playlist", value="playlist"), - ] - ) + async def query_autocomplete(self, interaction: Interaction, current: str) -> list[app_commands.Choice[str]]: + """Looks up song/album for autocomplete + + Args: + interaction: The interaction that started the command. + current: Current input into query. + """ + + results = [] + + if len(current) >= 3: + search = self.subsonic.searching.search(current, song_count=5, album_count=5, artist_count=0) + for song in search.songs: + results.append(app_commands.Choice(name=f"🎵 {(f"{song.artist.name} - " if song.artist is not None else "")}{song.title} [{self.seconds_to_str(song.duration)}]", value=f"song:{song.id}")) + for album in search.albums: + results.append(app_commands.Choice(name=f"🎶 {(f"{album.artist.name} - " if album.artist is not None else "")}{album.name} ({album.song_count} songs)", value=f"album:{album.id}")) + else: + results = [app_commands.Choice(name="Input 3 or more letters to search", value="")] + return results + + @app_commands.command(description="Add a song or album to the queue") + @app_commands.autocomplete(query=query_autocomplete) async def play( self, interaction: Interaction, query: str, - # Ignore the MyPy error because discord.py uses the type to add autocompletion - what: app_commands.Choice[str] = "song", # type: ignore ) -> None: """Add a song in the queue and start the playback if it's stop. Args: interaction: The interaction that started the command. - song_name: The name of the song. + query: The type of media to play and its id. """ - # await interaction.response.defer(thinking=True) - - # Extract the type of element to be search, taking care of the default value - choice = what if isinstance(what, str) else what.value - + voice_client = await self.get_voice_client(interaction, True) if voice_client is None: return - playing_element_name = query + choice, value = query.split(":") first_play = self.queue.length(interaction) == 0 and self.now_playing is None + playing_element_name = "" match choice: case "song": - songs = self.subsonic.searching.search(query, song_count=10, album_count=0, artist_count=0).songs - if songs is None: + song = self.subsonic.browsing.get_song(value) + if song is None: await self.send_error(interaction, [f"No songs found with the name: **{query}**"]) return - - song = songs[0] + if song.title is None: await self.send_error(interaction, [f"The song is missing the required metadata: {query}"]) return - + playing_element_name = song.title self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) case "album": - albums = self.subsonic.searching.search(query, song_count=0, album_count=10, artist_count=0).albums - if albums is None: + album = self.subsonic.browsing.get_album(value) + if album is None: await self.send_error(interaction, [f"No albums found with the name: **{query}**"]) return - - album = albums[0].generate() - print(album) + if album.songs is None: await self.send_error(interaction, [f"The album is missing the required metadata: {query}"]) return - + if album.name is not None: playing_element_name = album.name @@ -361,28 +362,49 @@ async def play( continue self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) + case _: + await self.send_error(interaction, [f"No songs found with the name: **{query}**"]) + return - case "playlist": - for playlist in self.subsonic.playlists.get_playlists(): - if playlist.name is None: - continue + if first_play: + await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}**"]) + else: + await self.send_answer(interaction, "🎧 Added to the queue", [f"**{playing_element_name}**"]) - if query in playlist.name: - playlist = playlist.generate() - if playlist.songs is None: - await self.send_error(interaction, ["The playlist has no songs!"]) - return + if not voice_client.is_playing(): + self.play_queue(interaction, None) - if playlist.name is not None: - playing_element_name = playlist.name + @app_commands.command(description="Adds a playlist to the queue") + async def playlist(self, interaction: Interaction, query: str) -> None: + """Queues a playlist + + Args: + interaction: The interaction that started the command. + query: The name of the playlist + """ + voice_client = await self.get_voice_client(interaction, True) + if voice_client is None: + return - for song in playlist.songs: - if song.title is None: - logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") - continue + first_play = self.queue.length(interaction) == 0 and self.now_playing is None + playing_element_name = query - self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) - break + for playlist in self.subsonic.playlists.get_playlists(): + if playlist.name is None: + continue + if query.lower() in playlist.name.lower(): + playlist = playlist.generate() + if playlist.songs is None: + await self.send_error(interaction, ["The playlist has no songs!"]) + return + if playlist.name is not None: + playing_element_name = playlist.name + for song in playlist.songs: + if song.title is None: + logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") + continue + self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) + break if first_play: await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}**"]) @@ -515,12 +537,12 @@ async def queue_command(self, interaction: Interaction, page: int = 1) -> None: page -= 1 if length > 0: - content.append(f"""Remaining time - {convert(self.queue.duration(interaction))} + content.append(f"""Remaining time - {self.seconds_to_str(self.queue.duration(interaction))} Pages - {page+1}/{max_page} Next:""") for num, song in enumerate(islice(self.queue.get(interaction), 10*page, 10*(page + 1))): - content.append(f"{10*page + num + 1}. {song.artist} - **{song.title}**\t[{convert(song.duration)}]") + content.append(f"{10*page + num + 1}. {song.artist} - **{song.title}**\t[{self.seconds_to_str(song.duration)}]") if length == 0: content.append("_Queue empty_") From 8361eb20b05eedc5c22783d99e7f2cf0b4620e6c Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 16:36:09 +0200 Subject: [PATCH 08/13] Leave command --- src/disopy/cogs/queue.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index cf29cfb..acd5c2a 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -498,6 +498,32 @@ async def resume(self, interaction: Interaction) -> None: self.play_queue(interaction, None) await self.send_answer(interaction, "▶️ Resuming the playback") + @app_commands.command(name="leave", description="Kick the bot from the voice call") + async def leave(self, interaction: Interaction) -> None: + user = interaction.user + if isinstance(user, discord.User): + await self.send_error(interaction, ["You are not a member of the guild, something has gone very wrong..."]) + return None + + if user.voice is None or user.voice.channel is None: + await self.send_error(interaction, ["You are not connected to any voice channel!"]) + return None + + guild = interaction.guild + if guild is None: + await self.send_error(interaction, ["We are not chatting in a guild, something has gone very wrong..."]) + return None + + if guild.voice_client is None: + await self.send_error(interaction, ["I'm not connected to a voice channel!"]) + return None + + if user.voice.channel != guild.voice_client.channel: + await self.send_error(interaction, ["Join the same voice channel where I am"]) + return None + + await guild.voice_client.disconnect() + @app_commands.command(name="shuffle", description="Shuffles the current queue") async def shuffle_command(self, interaction: Interaction) -> None: """Mixes the songs in the queue, if one exists From ea024841203ce4f642c99979d02c9b834e5e500d Mon Sep 17 00:00:00 2001 From: leamikmik Date: Thu, 11 Sep 2025 16:47:12 +0200 Subject: [PATCH 09/13] Leave command fix --- src/disopy/cogs/queue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index acd5c2a..20c6cf0 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -521,8 +521,10 @@ async def leave(self, interaction: Interaction) -> None: if user.voice.channel != guild.voice_client.channel: await self.send_error(interaction, ["Join the same voice channel where I am"]) return None - - await guild.voice_client.disconnect() + try: + await guild.voice_client.disconnect() + finally: + await self.send_answer(interaction, "🚪 Bot left", ["Goodbye."]) @app_commands.command(name="shuffle", description="Shuffles the current queue") async def shuffle_command(self, interaction: Interaction) -> None: From a47bed158e860c2ccdff0cae1ebbe15167ce82f4 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Sat, 13 Sep 2025 13:40:51 +0200 Subject: [PATCH 10/13] Added listening activity --- src/disopy/discord.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/disopy/discord.py b/src/disopy/discord.py index 2ecfe78..91903c0 100644 --- a/src/disopy/discord.py +++ b/src/disopy/discord.py @@ -62,7 +62,7 @@ def get_bot(subsonic: Subsonic, config: Config, options: Options) -> Bot: intents = discord.Intents.default() intents.message_content = True - bot = discord.ext.commands.Bot(f"!{APP_NAME_LOWER}", intents=intents) + bot = discord.ext.commands.Bot(f"!{APP_NAME_LOWER}", intents=intents, activity=discord.Activity(type=discord.ActivityType.listening, name="music")) @bot.event async def on_ready() -> None: From 2409967a24f0dda131284e9a6b55c1f71df4e074 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Sat, 13 Sep 2025 13:52:20 +0200 Subject: [PATCH 11/13] Fixed autocomplete and added loop functionality --- src/disopy/cogs/queue.py | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index 20c6cf0..b2eda5a 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -180,6 +180,7 @@ def __init__(self, bot: Bot, options: Options, subsonic: Subsonic, config: Confi self.queue = Queue() self.now_playing: Song | None = None + self.loop = 0 # 0: no loop; 1: loop queue; 2: loop track self.skip_next_autoplay = False @@ -235,11 +236,14 @@ def play_queue(self, interaction: Interaction, exception: Exception | None) -> N if exception is not None: raise exception - if self.queue.length(interaction) == 0: + if self.queue.length(interaction) == 0 and self.loop < 2: logger.info("The queue is empty") return - song = self.queue.pop(interaction) + song = self.queue.pop(interaction) if self.loop < 2 else self.now_playing + if self.loop == 1: + self.queue.append(interaction, song) + if song is None: logger.error("Unable to get the song for playback") return @@ -297,11 +301,29 @@ async def query_autocomplete(self, interaction: Interaction, current: str) -> li results = [] if len(current) >= 3: - search = self.subsonic.searching.search(current, song_count=5, album_count=5, artist_count=0) - for song in search.songs: - results.append(app_commands.Choice(name=f"🎵 {(f"{song.artist.name} - " if song.artist is not None else "")}{song.title} [{self.seconds_to_str(song.duration)}]", value=f"song:{song.id}")) - for album in search.albums: - results.append(app_commands.Choice(name=f"🎶 {(f"{album.artist.name} - " if album.artist is not None else "")}{album.name} ({album.song_count} songs)", value=f"album:{album.id}")) + search = self.subsonic.searching.search(current, song_count=5, album_count=5, artist_count=5) + if search.songs is not None: + for song in search.songs: + res = f"🎵 {(f"{song.artists[0].name} - " if song.artists[0].name is not None else "")}{song.title}" + duration = f" [{self.seconds_to_str(song.duration)}]" + # Trunctuate result length if over 100 characters + if len(res) + len(duration) > 100: + results.append(app_commands.Choice(name=res[:97 - len(duration)] + "..." + duration, value=f"song:{song.id}")) + else: + results.append(app_commands.Choice(name= res + duration, value=f"song:{song.id}")) + + if search.albums is not None: + for album in search.albums: + res = f"🎶 {(f"{album.artists[0].name} - " if album.artists[0] is not None else "")}{album.name}" + num_songs = f" ({album.song_count} songs)" + # Trunctuate result length if over 100 characters + if len(res) + len(num_songs) > 100: + results.append(app_commands.Choice(name=res[:97 - len(num_songs)] + "..." + num_songs, value=f"album:{album.id}")) + else: + results.append(app_commands.Choice(name=res + num_songs, value=f"album:{album.id}")) + + if len(results) == 0: + results = [app_commands.Choice(name="No result found :(", value="")] else: results = [app_commands.Choice(name="Input 3 or more letters to search", value="")] return results @@ -328,12 +350,13 @@ async def play( choice, value = query.split(":") first_play = self.queue.length(interaction) == 0 and self.now_playing is None playing_element_name = "" + songs_added = 0 match choice: case "song": song = self.subsonic.browsing.get_song(value) if song is None: - await self.send_error(interaction, [f"No songs found with the name: **{query}**"]) + await self.send_error(interaction, [f"No song found"]) return if song.title is None: @@ -346,7 +369,7 @@ async def play( case "album": album = self.subsonic.browsing.get_album(value) if album is None: - await self.send_error(interaction, [f"No albums found with the name: **{query}**"]) + await self.send_error(interaction, [f"No album found"]) return if album.songs is None: @@ -360,16 +383,17 @@ async def play( if song.title is None: logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") continue - + songs_added += 1 self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) + case _: - await self.send_error(interaction, [f"No songs found with the name: **{query}**"]) + await self.send_error(interaction, [f"No songs found"]) return if first_play: - await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}**"]) + await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}**{f" ({songs_added} songs added)" if songs_added > 0 else ""}"]) else: - await self.send_answer(interaction, "🎧 Added to the queue", [f"**{playing_element_name}**"]) + await self.send_answer(interaction, "🎧 Added to the queue", [f"**{playing_element_name}**{f" ({songs_added} songs added)" if songs_added > 0 else ""}"]) if not voice_client.is_playing(): self.play_queue(interaction, None) @@ -388,6 +412,7 @@ async def playlist(self, interaction: Interaction, query: str) -> None: first_play = self.queue.length(interaction) == 0 and self.now_playing is None playing_element_name = query + songs_added = 0 for playlist in self.subsonic.playlists.get_playlists(): if playlist.name is None: @@ -403,13 +428,14 @@ async def playlist(self, interaction: Interaction, query: str) -> None: if song.title is None: logger.error(f"The song with ID '{song.id}' is missing the name metadata entry") continue + songs_added += 1 self.queue.append(interaction, Song(song.id, song.title, song.artists[0].name, song.duration)) break if first_play: - await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}**"]) + await self.send_answer(interaction, "🎵 Now playing!", [f"**{playing_element_name}** ({songs_added} songs added)"]) else: - await self.send_answer(interaction, "🎧 Added to the queue", [f"**{playing_element_name}**"]) + await self.send_answer(interaction, "🎧 Added to the queue", [f"**{playing_element_name}** ({songs_added} songs added)"]) if not voice_client.is_playing(): self.play_queue(interaction, None) @@ -526,6 +552,41 @@ async def leave(self, interaction: Interaction) -> None: finally: await self.send_answer(interaction, "🚪 Bot left", ["Goodbye."]) + @app_commands.command(name="loop", description="Loops the queue") + @app_commands.choices( + what = [ + app_commands.Choice(name="Queue", value=1), + app_commands.Choice(name="Song", value=2) + ] + ) + async def loop_command(self, interaction: Interaction, what: app_commands.Choice[int] = 1) -> None: + """Loops the queue / current track + + Args: + interaction: The interaction that started the command. + what: How to loop queue + """ + voice_client = await self.get_voice_client(interaction) + if voice_client is None: + return + + if self.queue.length(interaction) == 0 and what != 2: + await self.send_error(interaction, ["The queue is empty"]) + return + if self.now_playing is None and what == 2: + await self.send_error(interaction, ["Nothing is playing"]) + return + + if what == self.loop: + self.loop = 0 + await self.send_answer(interaction, "🔁 Stopped looping") + + if what == 1 and self.now_playing is not None: + self.queue.append(interaction, self.now_playing) + + self.loop = what + await self.send_answer(interaction, "🔁 Now looping queue" if what == 1 else "🔂 Now looping current track") + @app_commands.command(name="shuffle", description="Shuffles the current queue") async def shuffle_command(self, interaction: Interaction) -> None: """Mixes the songs in the queue, if one exists From 2234ca3f3bcb89858647cf3e725e7fb9b3681d9a Mon Sep 17 00:00:00 2001 From: leamikmik Date: Sat, 13 Sep 2025 14:40:23 +0200 Subject: [PATCH 12/13] Made queue clear command and upon connecting --- src/disopy/cogs/queue.py | 49 ++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index b2eda5a..db1310e 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -158,7 +158,18 @@ def duration(self, interaction: Interaction) -> int: return 0 return sum(song.duration for song in self.queue[id]) - + + def clear(self, interaction: Interaction) -> None: + """Empties the queue + + Args: + interaction: The interaction where the guild ID can be found. + """ + id = self._check_guild(interaction) + if id is None: + return + + self.queue[id] = deque() class QueueCog(Base): """Cog that holds queue handling and music playback commands.""" @@ -201,6 +212,9 @@ async def get_voice_client(self, interaction: Interaction, connect: bool = False if guild.voice_client is None: if connect: + self.queue.clear(interaction) + self.now_playing = None + self.loop = 0 return await user.voice.channel.connect(self_deaf=True) await self.send_error(interaction, ["I'm not connected to a voice channel!"]) return None @@ -321,7 +335,7 @@ async def query_autocomplete(self, interaction: Interaction, current: str) -> li results.append(app_commands.Choice(name=res[:97 - len(num_songs)] + "..." + num_songs, value=f"album:{album.id}")) else: results.append(app_commands.Choice(name=res + num_songs, value=f"album:{album.id}")) - + if len(results) == 0: results = [app_commands.Choice(name="No result found :(", value="")] else: @@ -500,6 +514,20 @@ async def skip(self, interaction: Interaction) -> None: voice_client.stop() await self.send_answer(interaction, "⏭️ Song skipped") + @app_commands.command(description="Clear the queue") + async def clear(self, interaction: Interaction) -> None: + """Clears the remaining queue + + Args: + interaction: The interaction that started the command. + """ + voice_client = await self.get_voice_client(interaction) + if voice_client is None: + return + + self.queue.clear(interaction) + await self.send_answer(interaction, "🗑️ Cleared the queue") + @app_commands.command(description="Resume the playback") async def resume(self, interaction: Interaction) -> None: """Resume the playback of the song and if there is no one playing play the next one in the queue. @@ -524,7 +552,7 @@ async def resume(self, interaction: Interaction) -> None: self.play_queue(interaction, None) await self.send_answer(interaction, "▶️ Resuming the playback") - @app_commands.command(name="leave", description="Kick the bot from the voice call") + @app_commands.command(description="Kick the bot from the voice call") async def leave(self, interaction: Interaction) -> None: user = interaction.user if isinstance(user, discord.User): @@ -570,21 +598,22 @@ async def loop_command(self, interaction: Interaction, what: app_commands.Choice if voice_client is None: return - if self.queue.length(interaction) == 0 and what != 2: + if self.queue.length(interaction) == 0 and what.value != 2: await self.send_error(interaction, ["The queue is empty"]) return - if self.now_playing is None and what == 2: + if self.now_playing is None and what.value == 2: await self.send_error(interaction, ["Nothing is playing"]) return - if what == self.loop: + if what.value == self.loop: self.loop = 0 await self.send_answer(interaction, "🔁 Stopped looping") + return - if what == 1 and self.now_playing is not None: + if what.value == 1 and self.now_playing is not None and self.now_playing != self.queue[-1]: self.queue.append(interaction, self.now_playing) - self.loop = what + self.loop = what.value await self.send_answer(interaction, "🔁 Now looping queue" if what == 1 else "🔂 Now looping current track") @app_commands.command(name="shuffle", description="Shuffles the current queue") @@ -617,7 +646,7 @@ async def queue_command(self, interaction: Interaction, page: int = 1) -> None: max_page = ceil(self.queue.length(interaction)/10) length = self.queue.length(interaction) - if 1 > page or page > max_page: + if (1 > page or page > max_page) and max_page != 0: await self.send_error(interaction, ["Out of queue bounds"]) if self.now_playing is not None: @@ -625,6 +654,8 @@ async def queue_command(self, interaction: Interaction, page: int = 1) -> None: content.append("") page -= 1 + if self.loop > 0: + content.append(f"Looping {"queue" if self.loop == 1 else "track"}") if length > 0: content.append(f"""Remaining time - {self.seconds_to_str(self.queue.duration(interaction))} Pages - {page+1}/{max_page} From d0428f9b33a32270fdbf0bff529a01e86313f869 Mon Sep 17 00:00:00 2001 From: leamikmik Date: Sat, 13 Sep 2025 15:21:16 +0200 Subject: [PATCH 13/13] removed redundand artist search --- src/disopy/cogs/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/disopy/cogs/queue.py b/src/disopy/cogs/queue.py index db1310e..18ad17d 100644 --- a/src/disopy/cogs/queue.py +++ b/src/disopy/cogs/queue.py @@ -315,7 +315,7 @@ async def query_autocomplete(self, interaction: Interaction, current: str) -> li results = [] if len(current) >= 3: - search = self.subsonic.searching.search(current, song_count=5, album_count=5, artist_count=5) + search = self.subsonic.searching.search(current, song_count=5, album_count=5, artist_count=0) if search.songs is not None: for song in search.songs: res = f"🎵 {(f"{song.artists[0].name} - " if song.artists[0].name is not None else "")}{song.title}"