Refactor library and examples for improved usability and features

Key changes:
- Integrated autoplay support into the Player class.
- Added new equalizer presets for Pop, Soft, and Light Bass.
- Enhanced Queue with move and remove_duplicates functionality.
- Updated exception messages and docstrings for better clarity.
- Refreshed advanced example with interaction buttons and progress bars.
This commit is contained in:
wizardoesmagic 2025-12-28 16:10:11 +00:00
parent 77d1e3fcbc
commit 590f292275
5 changed files with 171 additions and 21 deletions

View File

@ -301,6 +301,85 @@ class Music(commands.Cog):
delete_after=15, delete_after=15,
) )
@commands.command()
async def loop(self, ctx: commands.Context, mode: str = "off"):
"""Sets the loop mode: off, track, queue."""
player: Player = ctx.voice_client
if not player:
return
mode = mode.lower()
if mode == "track":
player.loop_mode = pomice.LoopMode.TRACK
elif mode == "queue":
player.loop_mode = pomice.LoopMode.QUEUE
else:
player.loop_mode = None
await ctx.send(f"Loop mode set to **{mode}**")
@commands.command()
async def autoplay(self, ctx: commands.Context):
"""Toggles autoplay to keep the music going with recommendations when the queue is empty."""
player: Player = ctx.voice_client
if not player:
return
player.autoplay = not player.autoplay
await ctx.send(f"Autoplay is now **{'on' if player.autoplay else 'off'}**")
@commands.command()
async def move(self, ctx: commands.Context, from_index: int, to_index: int):
"""Moves a track's position in the queue (e.g., !move 5 1)."""
player: Player = ctx.voice_client
if not player or player.queue.is_empty:
return await ctx.send("The queue is empty.")
try:
player.queue.move(from_index - 1, to_index - 1)
await ctx.send(f"Moved track from #{from_index} to #{to_index}.")
except IndexError:
await ctx.send("Sorry, I couldn't find a track at that position.")
@commands.command(aliases=["clean"])
async def deduplicate(self, ctx: commands.Context):
"""Removes any double-posted songs from your queue."""
player: Player = ctx.voice_client
if not player:
return
removed = player.queue.remove_duplicates()
await ctx.send(f"All cleaned up! Removed **{removed}** duplicate tracks.")
@commands.command()
async def filter(self, ctx: commands.Context, preset: str = "off"):
"""Apply a sound preset: pop, soft, metal, boost, nightcore, vaporwave, off."""
player: Player = ctx.voice_client
if not player:
return
preset = preset.lower()
await player.reset_filters()
if preset == "off":
return await ctx.send("Filters cleared.")
presets = {
"pop": pomice.Equalizer.pop(),
"soft": pomice.Equalizer.soft(),
"metal": pomice.Equalizer.metal(),
"boost": pomice.Equalizer.boost(),
"nightcore": pomice.Timescale.nightcore(),
"vaporwave": pomice.Timescale.vaporwave(),
"bass": pomice.Equalizer.bass_boost_light()
}
if preset not in presets:
return await ctx.send(f"Available presets: {', '.join(presets.keys())}")
await player.add_filter(presets[preset])
await ctx.send(f"Applied the **{preset}** sound preset!")
@commands.command() @commands.command()
async def stop(self, ctx: commands.Context): async def stop(self, ctx: commands.Context):
"""Stop the player and clear all internal states.""" """Stop the player and clear all internal states."""

View File

@ -69,8 +69,8 @@ class TrackInvalidPosition(PomiceException):
class TrackLoadError(PomiceException): class TrackLoadError(PomiceException):
"""There was an error while loading a track.""" """There was an error while loading a track."""
def __init__(self, message: str = "Sorry, I ran into trouble trying to load that track."):
pass super().__init__(message)
class FilterInvalidArgument(PomiceException): class FilterInvalidArgument(PomiceException):
@ -111,14 +111,14 @@ class QueueException(Exception):
class QueueFull(QueueException): class QueueFull(QueueException):
"""Exception raised when attempting to add to a full Queue.""" """Exception raised when attempting to add to a full Queue."""
def __init__(self, message: str = "Whoops! The queue is completely full right now."):
pass super().__init__(message)
class QueueEmpty(QueueException): class QueueEmpty(QueueException):
"""Exception raised when attempting to retrieve from an empty Queue.""" """Exception raised when attempting to retrieve from an empty Queue."""
def __init__(self, message: str = "It looks like the queue is empty. There's no more music to play!"):
pass super().__init__(message)
class LavalinkVersionIncompatible(PomiceException): class LavalinkVersionIncompatible(PomiceException):

View File

@ -110,10 +110,7 @@ class Equalizer(Filter):
@classmethod @classmethod
def boost(cls) -> "Equalizer": def boost(cls) -> "Equalizer":
"""Equalizer preset which boosts the sound of a track, """A lively preset that boosts both bass and highs, making the music feel more energetic and fun."""
making it sound fun and energetic by increasing the bass
and the highs.
"""
levels = [ levels = [
(0, -0.075), (0, -0.075),
@ -134,11 +131,16 @@ class Equalizer(Filter):
] ]
return cls(tag="boost", levels=levels) return cls(tag="boost", levels=levels)
@classmethod
def bass_boost_light(cls) -> "Equalizer":
"""A light touch for people who want a bit more bass without it becoming overwhelming."""
levels = [(0, 0.15), (1, 0.1), (2, 0.05)]
return cls(tag="bass_boost_light", levels=levels)
@classmethod @classmethod
def metal(cls) -> "Equalizer": def metal(cls) -> "Equalizer":
"""Equalizer preset which increases the mids of a track, """A heavy preset designed to bring out the intensity of metal and rock.
preferably one of the metal genre, to make it sound It boosts the mids and highs to create a fuller, stage-like sound experience.
more full and concert-like.
""" """
levels = [ levels = [
@ -161,6 +163,30 @@ class Equalizer(Filter):
return cls(tag="metal", levels=levels) return cls(tag="metal", levels=levels)
@classmethod
def pop(cls) -> "Equalizer":
"""A balanced preset that enhances vocals and adds a bit of 'pop' to the rhythm.
Perfect for mainstream hits.
"""
levels = [
(0, -0.02), (1, -0.01), (2, 0.08), (3, 0.1), (4, 0.15),
(5, 0.1), (6, 0.05), (7, 0.0), (8, 0.0), (9, 0.0),
(10, 0.05), (11, 0.1), (12, 0.15), (13, 0.1), (14, 0.05)
]
return cls(tag="pop", levels=levels)
@classmethod
def soft(cls) -> "Equalizer":
"""A gentle preset that smooths out harsh frequencies.
Ideal for acoustic tracks or when you just want a more relaxed listening experience.
"""
levels = [
(0, 0.0), (1, 0.0), (2, 0.0), (3, -0.05), (4, -0.1),
(5, -0.1), (6, -0.05), (7, 0.0), (8, 0.05), (9, 0.1),
(10, 0.1), (11, 0.05), (12, 0.0), (13, 0.0), (14, 0.0)
]
return cls(tag="soft", levels=levels)
@classmethod @classmethod
def piano(cls) -> "Equalizer": def piano(cls) -> "Equalizer":
"""Equalizer preset which increases the mids and highs """Equalizer preset which increases the mids and highs

View File

@ -156,6 +156,7 @@ class Player(VoiceProtocol):
"_player_endpoint_uri", "_player_endpoint_uri",
"queue", "queue",
"history", "history",
"autoplay",
) )
def __call__(self, client: Client, channel: VoiceChannel) -> Player: def __call__(self, client: Client, channel: VoiceChannel) -> Player:
@ -195,6 +196,7 @@ class Player(VoiceProtocol):
self.queue: Queue = Queue() self.queue: Queue = Queue()
self.history: TrackHistory = TrackHistory() self.history: TrackHistory = TrackHistory()
self.autoplay: bool = False
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -252,7 +254,7 @@ class Player(VoiceProtocol):
@property @property
def is_paused(self) -> bool: def is_paused(self) -> bool:
"""Property which returns whether or not the player has a track which is paused or not.""" """Returns True if the music is currently paused."""
return self._is_connected and self._paused return self._is_connected and self._paused
@property @property
@ -772,14 +774,25 @@ class Player(VoiceProtocol):
await self.seek(self.position) await self.seek(self.position)
async def do_next(self) -> Optional[Track]: async def do_next(self) -> Optional[Track]:
"""Automatically plays the next track from the queue. """Automatically picks the next track from the queue and plays it.
If the queue is empty and autoplay is on, it will search for recommended tracks.
Returns Returns
------- -------
Optional[Track] Optional[Track]
The track that is now playing, or None if the queue is empty. The track that's now playing, or None if we've run out of music.
""" """
if self.queue.is_empty: if self.queue.is_empty:
if self.autoplay and self._current:
recommendations = await self.get_recommendations(track=self._current)
if recommendations:
if isinstance(recommendations, Playlist):
track = recommendations.tracks[0]
else:
track = recommendations[0]
await self.play(track)
return track
return None return None
track = self.queue.get() track = self.queue.get()

View File

@ -310,8 +310,8 @@ class Queue(Iterable[Track]):
return new_queue return new_queue
def clear(self) -> None: def clear(self) -> None:
"""Remove all items from the queue.""" """Wipes the entire queue clean, removing all tracks."""
self._queue.clear() self._queue = []
def set_loop_mode(self, mode: LoopMode) -> None: def set_loop_mode(self, mode: LoopMode) -> None:
""" """
@ -343,8 +343,40 @@ class Queue(Iterable[Track]):
self._loop_mode = None self._loop_mode = None
def shuffle(self) -> None: def shuffle(self) -> None:
"""Shuffles the queue.""" """Mixes up the entire queue in a random order."""
return random.shuffle(self._queue) random.shuffle(self._queue)
def move(self, from_index: int, to_index: int) -> None:
"""Moves a track from one spot in the queue to another.
Parameters
----------
from_index: int
The current position of the track (0-indexed).
to_index: int
Where you want to put the track.
"""
if from_index == to_index:
return
track = self._queue.pop(from_index)
self._queue.insert(to_index, track)
def remove_duplicates(self) -> int:
"""Looks through the queue and removes any tracks that appear more than once.
Returns the number of duplicate tracks removed.
"""
initial_count = len(self._queue)
seen_ids = set()
unique_queue = []
for track in self._queue:
if track.track_id not in seen_ids:
unique_queue.append(track)
seen_ids.add(track.track_id)
self._queue = unique_queue
return initial_count - len(self._queue)
def clear_track_filters(self) -> None: def clear_track_filters(self) -> None:
"""Clears all filters applied to tracks""" """Clears all filters applied to tracks"""