queue, int position)
+ {
+ return position < 1 || position > queue.size();
+ }
+
+ // ========== Inner Classes ==========
+
+ /**
+ * Result of adding a track to the queue.
+ */
+ public static class TrackAddResult
+ {
+ public final int position;
+ public final String formattedMessage;
+ public final String trackTitle;
+
+ public TrackAddResult(int position, String formattedMessage, String trackTitle)
+ {
+ this.position = position;
+ this.formattedMessage = formattedMessage;
+ this.trackTitle = trackTitle;
+ }
+ }
+
+ /**
+ * Data class containing queue information for display.
+ */
+ public static class QueueInfo
+ {
+ public final String[] tracks;
+ public final long totalDuration;
+ public final String nowPlayingTitle;
+ public final String statusEmoji;
+ public final RepeatMode repeatMode;
+ public final QueueType queueType;
+ public final Object nowPlayingMessage;
+ public final Object noMusicMessage;
+
+ public QueueInfo(String[] tracks, long totalDuration, String nowPlayingTitle, String statusEmoji,
+ RepeatMode repeatMode, QueueType queueType, Object nowPlayingMessage, Object noMusicMessage)
+ {
+ this.tracks = tracks;
+ this.totalDuration = totalDuration;
+ this.nowPlayingTitle = nowPlayingTitle;
+ this.statusEmoji = statusEmoji;
+ this.repeatMode = repeatMode;
+ this.queueType = queueType;
+ this.nowPlayingMessage = nowPlayingMessage;
+ this.noMusicMessage = noMusicMessage;
+ }
+
+ public boolean isEmpty()
+ {
+ return tracks.length == 0;
+ }
+ }
+
+ /**
+ * Adapter interface for abstracting output operations.
+ *
+ * This interface allows services to be command-type agnostic - the same service
+ * methods work for text commands, slash commands, and button interactions.
+ * Each command type provides its own implementation.
+ *
+ * @see com.jagrosh.jmusicbot.commands.BaseOutputAdapter
+ * @see com.jagrosh.jmusicbot.commands.v1.TextOutputAdapters
+ * @see com.jagrosh.jmusicbot.commands.v2.SlashOutputAdapters
+ */
+ public interface OutputAdapter
+ {
+ void replySuccess(String content);
+ void replyError(String content);
+ void replyWarning(String content);
+ void editMessage(String content);
+ void editMessage(String content, Consumer onSuccess);
+ void editNowPlaying(AudioHandler handler);
+ void editNoMusic(AudioHandler handler);
+ void onShowHelp();
+ }
+
+ private class ResultHandler implements AudioLoadResultHandler
+ {
+ private final static String LOAD = "\uD83D\uDCE5"; // 📥
+ private final static String CANCEL = "\uD83D\uDEAB"; // 🚫
+
+ private final OutputAdapter output;
+ private final Guild guild;
+ private final Member member;
+ private final String args;
+ private final boolean ytsearch;
+ private final TextChannel channel;
+
+ private ResultHandler(OutputAdapter output, Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
+ {
+ this.output = output;
+ this.guild = guild;
+ this.member = member;
+ this.args = args;
+ this.ytsearch = ytsearch;
+ this.channel = channel;
+ }
+
+ private void loadSingle(AudioTrack track, AudioPlaylist playlist)
+ {
+ TrackAddResult result = addTrackToQueue(guild, member, track, args, channel);
+ if (result == null)
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " " + formatTooLongError(track)));
+ return;
+ }
+
+ String addMsg = FormatUtil.filter(bot.getConfig().getSuccess() + " " + result.formattedMessage);
+ if (playlist == null || !guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_ADD_REACTION))
+ output.editMessage(addMsg);
+ else
+ {
+ String promptMsg = addMsg + "\n" + bot.getConfig().getWarning() + " This track has a playlist of **" + playlist.getTracks().size() + "** tracks attached. Select " + LOAD + " to load playlist.";
+
+ MessageEditBuilder editBuilder = new MessageEditBuilder()
+ .setContent(promptMsg)
+ .setComponents(ActionRow.of(
+ Button.success("load_playlist", Emoji.fromUnicode(LOAD)).withLabel("Load Playlist"),
+ Button.danger("cancel_playlist", Emoji.fromUnicode(CANCEL)).withLabel("Cancel")
+ ));
+
+ output.editMessage(addMsg, m -> {
+ m.editMessage(editBuilder.build()).queue(msg -> {
+ bot.getWaiter().waitForEvent(ButtonInteractionEvent.class,
+ event -> event.getMessageId().equals(msg.getId()) &&
+ (event.getComponentId().equals("load_playlist") || event.getComponentId().equals("cancel_playlist")) &&
+ event.getUser().getIdLong() == member.getIdLong(),
+ event -> {
+ if (event.getComponentId().equals("load_playlist"))
+ {
+ int loaded = loadPlaylist(playlist, track);
+ event.editMessage(addMsg + "\n" + bot.getConfig().getSuccess() + " Loaded **" + loaded + "** additional tracks!").setComponents().queue();
+ }
+ else
+ {
+ event.editMessage(addMsg).setComponents().queue();
+ }
+ },
+ 30, TimeUnit.SECONDS,
+ () -> msg.editMessage(addMsg).setComponents().queue());
+ });
+ });
+ }
+ }
+
+ private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
+ {
+ int[] count = {0};
+ AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ playlist.getTracks().forEach((track) -> {
+ if (!isTooLong(track) && !track.equals(exclude))
+ {
+ handler.setLastReason(member.getUser().getName() + " added a playlist.");
+ handler.addTrack(new QueuedTrack(track,
+ new RequestMetadata(member.getUser(),
+ new RequestMetadata.RequestInfo(args, track.getInfo().uri),
+ channel.getIdLong())));
+ count[0]++;
+ }
+ });
+ return count[0];
+ }
+
+ @Override
+ public void trackLoaded(AudioTrack track)
+ {
+ loadSingle(track, null);
+ }
+
+ @Override
+ public void playlistLoaded(AudioPlaylist playlist)
+ {
+ if (playlist.getTracks().size() == 1 || playlist.isSearchResult())
+ {
+ AudioTrack single = playlist.getSelectedTrack() == null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
+ loadSingle(single, null);
+ }
+ else if (playlist.getSelectedTrack() != null)
+ {
+ AudioTrack single = playlist.getSelectedTrack();
+ loadSingle(single, playlist);
+ }
+ else
+ {
+ int count = loadPlaylist(playlist, null);
+ if (playlist.getTracks().size() == 0)
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " The playlist " + (playlist.getName() == null ? "" : "(**" + playlist.getName()
+ + "**) ") + " could not be loaded or contained 0 entries"));
+ }
+ else if (count == 0)
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " All entries in this playlist " + (playlist.getName() == null ? "" : "(**" + playlist.getName()
+ + "**) ") + "were longer than the allowed maximum (`" + bot.getConfig().getMaxTime() + "`)"));
+ }
+ else
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getSuccess() + " Found "
+ + (playlist.getName() == null ? "a playlist" : "playlist **" + playlist.getName() + "**") + " with `"
+ + playlist.getTracks().size() + "` entries; added to the queue!"
+ + (count < playlist.getTracks().size() ? "\n" + bot.getConfig().getWarning() + " Tracks longer than the allowed maximum (`"
+ + bot.getConfig().getMaxTime() + "`) have been omitted." : "")));
+ }
+ }
+ }
+
+ @Override
+ public void noMatches()
+ {
+ if (ytsearch)
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " No results found for `" + args + "`."));
+ else
+ bot.getPlayerManager().loadItemOrdered(guild, "ytsearch:" + args, new ResultHandler(output, guild, member, args, true, channel));
+ }
+
+ @Override
+ public void loadFailed(FriendlyException throwable)
+ {
+ if (throwable.severity == Severity.COMMON)
+ output.editMessage(bot.getConfig().getError() + " Error loading: " + throwable.getMessage());
+ else
+ output.editMessage(bot.getConfig().getError() + " Error loading track.");
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/service/PlayerService.java b/src/main/java/com/jagrosh/jmusicbot/service/PlayerService.java
deleted file mode 100644
index 56b1b505a..000000000
--- a/src/main/java/com/jagrosh/jmusicbot/service/PlayerService.java
+++ /dev/null
@@ -1,389 +0,0 @@
-package com.jagrosh.jmusicbot.service;
-
-import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.QueuedTrack;
-import com.jagrosh.jmusicbot.audio.RequestMetadata;
-import com.jagrosh.jmusicbot.commands.v1.DJCommand;
-import com.jagrosh.jmusicbot.utils.FormatUtil;
-import com.jagrosh.jmusicbot.utils.TimeUtil;
-import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
-import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
-import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
-import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
-import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import com.jagrosh.jmusicbot.settings.RepeatMode;
-import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.components.actionrow.ActionRow;
-import net.dv8tion.jda.api.components.buttons.Button;
-import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.Message;
-import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
-import net.dv8tion.jda.api.entities.emoji.Emoji;
-import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
-import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
-
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-
-public class PlayerService
-{
- private final Bot bot;
-
- public PlayerService(Bot bot)
- {
- this.bot = bot;
- }
-
- public void play(Guild guild, Member member, String args, TextChannel channel, OutputAdapter output)
- {
- if (args != null && args.startsWith("\"") && args.endsWith("\""))
- args = args.substring(1, args.length() - 1);
-
- if (args == null || args.isEmpty())
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- if (handler.getPlayer().getPlayingTrack() != null && handler.getPlayer().isPaused())
- {
- if (DJCommand.checkDJPermission(bot, guild, member))
- {
- handler.getPlayer().setPaused(false);
- output.replySuccess("Resumed **" + handler.getPlayer().getPlayingTrack().getInfo().title + "**.");
- }
- else
- output.replyError("Only DJs can unpause the player!");
- return;
- }
- output.onShowHelp();
- return;
- }
-
- bot.getPlayerManager().loadItemOrdered(guild, args, new ResultHandler(output, guild, member, args, false, channel));
- }
-
- public void previous(Guild guild, Member member, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ && handler.getRequestMetadata().getOwner() != member.getIdLong())
- {
- output.replyError("You need to be a DJ or the requester to go back!");
- return;
- }
- AudioTrack playing = handler.getPlayer().getPlayingTrack();
-
- // If the track has played for more than 5 seconds, restart it
- if (playing != null && playing.getPosition() > 5000)
- {
- playing.setPosition(0);
- output.replySuccess("Restarted **" + playing.getInfo().title + "**");
- return;
- }
-
- // Check if there's history available
- if (handler.getQueue().getHistory().isEmpty())
- {
- output.replyError("There are no previous tracks!");
- return;
- }
-
- // Use queue's rewind method to go back to previous track
- AudioTrack currentlyPlaying = handler.getPlayer().getPlayingTrack();
- QueuedTrack currentQueued = currentlyPlaying != null
- ? new QueuedTrack(currentlyPlaying.makeClone(), handler.getRequestMetadata())
- : null;
-
- QueuedTrack previous = handler.getQueue().rewind(currentQueued);
- if (previous != null)
- {
- handler.getPlayer().playTrack(previous.getTrack());
- output.replySuccess("Went back to **" + previous.getTrack().getInfo().title + "**");
- }
- else
- {
- output.replyError("There are no previous tracks!");
- }
- }
-
- public void shuffle(Guild guild, Member member, int startIndex, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
- return;
- }
- int s = handler.getQueue().shuffle(startIndex);
- output.replySuccess("Shuffled " + s + " tracks!");
- }
-
- public void cycleRepeatMode(Guild guild, Member member, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
- return;
- }
- RepeatMode mode = bot.getSettingsManager().getSettings(guild).getRepeatMode();
- RepeatMode nextMode;
- switch (mode) {
- case OFF:
- nextMode = RepeatMode.ALL;
- break;
- case ALL:
- nextMode = RepeatMode.SINGLE;
- break;
- case SINGLE:
- default:
- nextMode = RepeatMode.OFF;
- break;
- }
- bot.getSettingsManager().getSettings(guild).setRepeatMode(nextMode);
- output.editNowPlaying(handler);
- }
-
- public void adjustVolume(Guild guild, Member member, int change, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
- return;
- }
- int newVol = handler.getPlayer().getVolume() + change;
- newVol = Math.max(0, Math.min(150, newVol));
- handler.getPlayer().setVolume(newVol);
- bot.getSettingsManager().getSettings(guild).setVolume(newVol);
- output.editNowPlaying(handler);
- }
-
- public void stop(Guild guild, Member member, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
- return;
- }
- handler.stopAndClear();
- guild.getAudioManager().closeAudioConnection();
- output.editNoMusic(handler);
- }
-
- public void pause(Guild guild, Member member, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
- return;
- }
- handler.getPlayer().setPaused(!handler.getPlayer().isPaused());
- output.editNowPlaying(handler);
- }
-
- public void skip(Guild guild, Member member, OutputAdapter output)
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- RequestMetadata skipRm = handler.getRequestMetadata();
- if (!isDJ && skipRm.getOwner() != member.getIdLong())
- {
- output.replyError("You need to be a DJ or the requester to skip!");
- return;
- }
- if (bot.getSettingsManager().getSettings(guild).getRepeatMode() == RepeatMode.ALL)
- {
- var track = handler.getPlayer().getPlayingTrack();
- if (track != null)
- handler.addTrack(new QueuedTrack(track.makeClone(), track.getUserData(RequestMetadata.class)));
- }
- handler.setLastReason(member.getUser().getName() + " skipped forward.");
- handler.getPlayer().stopTrack();
- output.replySuccess("Skipped!");
- }
-
- public interface OutputAdapter
- {
- void replySuccess(String content);
- void replyError(String content);
- void replyWarning(String content);
- void editMessage(String content);
- void editMessage(String content, Consumer onSuccess);
- void editNowPlaying(AudioHandler handler);
- void editNoMusic(AudioHandler handler);
- void onShowHelp();
- }
-
- private class ResultHandler implements AudioLoadResultHandler
- {
- private final static String LOAD = "\uD83D\uDCE5"; // 📥
- private final static String CANCEL = "\uD83D\uDEAB"; // 🚫
-
- private final OutputAdapter output;
- private final Guild guild;
- private final Member member;
- private final String args;
- private final boolean ytsearch;
- private final TextChannel channel;
-
- private ResultHandler(OutputAdapter output, Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
- {
- this.output = output;
- this.guild = guild;
- this.member = member;
- this.args = args;
- this.ytsearch = ytsearch;
- this.channel = channel;
- }
-
- private void loadSingle(AudioTrack track, AudioPlaylist playlist)
- {
-
- // Get the formatted title (handles local files)
- String title = FormatUtil.getTrackTitle(track);
-
- if(bot.getConfig().isTooLong(track))
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning()+" This track (**"+title+"**) is longer than the allowed maximum: `"
- + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`"));
- return;
- }
-
-
-
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- handler.setLastReason(member.getUser().getName() + " added to the queue.");
- int pos = handler.addTrack(new QueuedTrack(track, new RequestMetadata(member.getUser(), new RequestMetadata.RequestInfo(args, track.getInfo().uri), channel.getIdLong()))) + 1;
- String addMsg = FormatUtil.filter(bot.getConfig().getSuccess()+" Added **"+title
- +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
- if(playlist==null || !guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_ADD_REACTION))
- output.editMessage(addMsg);
- else
- {
- String promptMsg = addMsg + "\n" + bot.getConfig().getWarning() + " This track has a playlist of **" + playlist.getTracks().size() + "** tracks attached. Select " + LOAD + " to load playlist.";
-
- // Use native JDA buttons instead of deprecated ButtonMenu
- MessageEditBuilder editBuilder = new MessageEditBuilder()
- .setContent(promptMsg)
- .setComponents(ActionRow.of(
- Button.success("load_playlist", Emoji.fromUnicode(LOAD)).withLabel("Load Playlist"),
- Button.danger("cancel_playlist", Emoji.fromUnicode(CANCEL)).withLabel("Cancel")
- ));
-
- output.editMessage(addMsg, m -> {
- m.editMessage(editBuilder.build()).queue(msg -> {
- // Wait for button interaction
- bot.getWaiter().waitForEvent(ButtonInteractionEvent.class,
- event -> event.getMessageId().equals(msg.getId()) &&
- (event.getComponentId().equals("load_playlist") || event.getComponentId().equals("cancel_playlist")) &&
- event.getUser().getIdLong() == member.getIdLong(),
- event -> {
- if (event.getComponentId().equals("load_playlist"))
- {
- int loaded = loadPlaylist(playlist, track);
- event.editMessage(addMsg + "\n" + bot.getConfig().getSuccess() + " Loaded **" + loaded + "** additional tracks!").setComponents().queue();
- }
- else
- {
- event.editMessage(addMsg).setComponents().queue();
- }
- },
- 30, TimeUnit.SECONDS,
- () -> msg.editMessage(addMsg).setComponents().queue());
- });
- });
- }
- }
-
- private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
- {
- int[] count = {0};
- playlist.getTracks().stream().forEach((track) -> {
- if(!bot.getConfig().isTooLong(track) && !track.equals(exclude))
- {
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- handler.setLastReason(member.getUser().getName() + " added a playlist.");
- handler.addTrack(new QueuedTrack(track, new RequestMetadata(member.getUser(), new RequestMetadata.RequestInfo(args, track.getInfo().uri), channel.getIdLong())));
- count[0]++;
- }
- });
- return count[0];
- }
-
- @Override
- public void trackLoaded(AudioTrack track)
- {
- loadSingle(track, null);
- }
-
- @Override
- public void playlistLoaded(AudioPlaylist playlist)
- {
- if(playlist.getTracks().size()==1 || playlist.isSearchResult())
- {
- AudioTrack single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
- loadSingle(single, null);
- }
- else if (playlist.getSelectedTrack()!=null)
- {
- AudioTrack single = playlist.getSelectedTrack();
- loadSingle(single, playlist);
- }
- else
- {
- int count = loadPlaylist(playlist, null);
- if(playlist.getTracks().size() == 0)
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning()+" The playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
- +"**) ")+" could not be loaded or contained 0 entries"));
- }
- else if(count==0)
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning()+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName()
- +"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)"));
- }
- else
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getSuccess()+" Found "
- +(playlist.getName()==null?"a playlist":"playlist **"+playlist.getName()+"**")+" with `"
- + playlist.getTracks().size()+"` entries; added to the queue!"
- + (count tracks, int limit)
+ {
+ int count = Math.min(limit, tracks.size());
+ String[] choices = new String[count];
+ for (int i = 0; i < count; i++)
+ {
+ AudioTrack track = tracks.get(i);
+ choices[i] = "`[" + TimeUtil.formatTime(track.getDuration()) + "]` [**"
+ + track.getInfo().title + "**](" + track.getInfo().uri + ")";
+ }
+ return choices;
+ }
+
+ /**
+ * Callback interface for search results.
+ */
+ public interface SearchCallback
+ {
+ /**
+ * Called when a single track is loaded.
+ */
+ void onTrackLoaded(AudioTrack track, int queuePosition, String formattedMessage);
+
+ /**
+ * Called when search results (playlist) are loaded.
+ */
+ void onSearchResults(AudioPlaylist playlist, String[] formattedChoices);
+
+ /**
+ * Called when no matches are found.
+ */
+ void onNoMatches(String query);
+
+ /**
+ * Called when loading fails.
+ */
+ void onLoadFailed(String errorMessage);
+
+ /**
+ * Called for general errors.
+ */
+ void onError(String message);
+ }
+
+ private class SearchResultHandler implements AudioLoadResultHandler
+ {
+ private final Guild guild;
+ private final Member member;
+ private final String query;
+ private final TextChannel channel;
+ private final SearchCallback callback;
+
+ private SearchResultHandler(Guild guild, Member member, String query,
+ TextChannel channel, SearchCallback callback)
+ {
+ this.guild = guild;
+ this.member = member;
+ this.query = query;
+ this.channel = channel;
+ this.callback = callback;
+ }
+
+ @Override
+ public void trackLoaded(AudioTrack track)
+ {
+ MusicService musicService = bot.getMusicService();
+
+ // Use shared track operations from MusicService
+ MusicService.TrackAddResult result = musicService.addTrackToQueue(guild, member, track, query, channel);
+ if (result == null)
+ {
+ callback.onLoadFailed(musicService.formatTooLongError(track));
+ return;
+ }
+
+ callback.onTrackLoaded(track, result.position, result.formattedMessage);
+ }
+
+ @Override
+ public void playlistLoaded(AudioPlaylist playlist)
+ {
+ String[] choices = formatSearchChoices(playlist.getTracks(), 4);
+ callback.onSearchResults(playlist, choices);
+ }
+
+ @Override
+ public void noMatches()
+ {
+ callback.onNoMatches(query);
+ }
+
+ @Override
+ public void loadFailed(FriendlyException throwable)
+ {
+ if (throwable.severity == Severity.COMMON)
+ {
+ callback.onLoadFailed("Error loading: " + throwable.getMessage());
+ }
+ else
+ {
+ callback.onLoadFailed("Error loading track.");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java b/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
index 2902be3f1..4dcc1bc2f 100644
--- a/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
+++ b/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
@@ -16,6 +16,7 @@
package com.jagrosh.jmusicbot.unit;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
+import dev.lavalink.youtube.clients.skeleton.Client;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -217,21 +218,27 @@ void youtubeClientsConfiguredForOAuthMode() throws Exception {
// With OAuth enabled
Object[] oauthClients = (Object[]) buildClients.invoke(null, true);
assertNotNull(oauthClients, "Should return clients for OAuth mode");
- assertEquals(5, oauthClients.length, "OAuth mode should use 5 clients (3 metadata-only: AndroidVr, MWeb, Web; 2 OAuth: TvHtml5Embedded, Tv)");
+ assertEquals(5, oauthClients.length, "OAuth mode should use 5 clients (AndroidVr, MWeb, Web, TvHtml5Embedded, Tv)");
- // Verify client types - first 3 are metadata-only (playback disabled)
+ // Verify client types
assertEquals("AndroidVr", oauthClients[0].getClass().getSimpleName(),
- "First OAuth client should be AndroidVr (metadata-only)");
+ "First OAuth client should be AndroidVr");
assertEquals("MWeb", oauthClients[1].getClass().getSimpleName(),
- "Second OAuth client should be MWeb (metadata-only)");
+ "Second OAuth client should be MWeb");
assertEquals("Web", oauthClients[2].getClass().getSimpleName(),
- "Third OAuth client should be Web (metadata-only)");
-
- // Last 2 are OAuth clients for streaming
+ "Third OAuth client should be Web");
assertEquals("TvHtml5Embedded", oauthClients[3].getClass().getSimpleName(),
- "Fourth OAuth client should be TvHtml5Embedded (OAuth streaming)");
+ "Fourth OAuth client should be TvHtml5Embedded");
assertEquals("Tv", oauthClients[4].getClass().getSimpleName(),
- "Fifth OAuth client should be Tv (OAuth streaming)");
+ "Fifth OAuth client should be Tv");
+
+ // Verify first 3 clients have playback disabled (metadataOnly)
+ for (int i = 0; i < 3; i++) {
+ Client client = (Client) oauthClients[i];
+ assertFalse(client.getOptions().getPlayback(),
+ String.format("OAuth client %d (%s) should have playback disabled",
+ i, client.getClass().getSimpleName()));
+ }
}
@Test
diff --git a/src/test/java/com/jagrosh/jmusicbot/unit/service/PlayerServiceTest.java b/src/test/java/com/jagrosh/jmusicbot/unit/service/PlayerServiceTest.java
index d46113c15..01e2c754b 100644
--- a/src/test/java/com/jagrosh/jmusicbot/unit/service/PlayerServiceTest.java
+++ b/src/test/java/com/jagrosh/jmusicbot/unit/service/PlayerServiceTest.java
@@ -1,7 +1,7 @@
package com.jagrosh.jmusicbot.unit.service;
import com.jagrosh.jmusicbot.TestBase;
-import com.jagrosh.jmusicbot.service.PlayerService;
+import com.jagrosh.jmusicbot.service.MusicService;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -11,15 +11,15 @@
public class PlayerServiceTest extends TestBase {
- private PlayerService.OutputAdapter output = mock(PlayerService.OutputAdapter.class);
+ private MusicService.OutputAdapter output = mock(MusicService.OutputAdapter.class);
- private PlayerService playerService;
+ private MusicService musicService;
@BeforeEach
@Override
public void setUp() {
super.setUp();
- playerService = new PlayerService(bot);
+ musicService = new MusicService(bot);
}
@Test
@@ -29,7 +29,7 @@ public void testPlayResumeWhenPaused() {
AudioTrackInfo info = new AudioTrackInfo("Title", "Author", 1000, "identifier", true, "uri");
when(audioTrack.getInfo()).thenReturn(info);
- playerService.play(guild, member, null, textChannel, output);
+ musicService.play(guild, member, null, textChannel, output);
verify(audioPlayer).setPaused(false);
verify(output).replySuccess(anyString());
@@ -39,7 +39,7 @@ public void testPlayResumeWhenPaused() {
public void testPlayWithArgsCallsLoadItem() {
String args = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; // this is the best song ever
- playerService.play(guild, member, args, textChannel, output);
+ musicService.play(guild, member, args, textChannel, output);
verify(playerManager).loadItemOrdered(eq(guild), eq(args), any());
}
@@ -48,7 +48,7 @@ public void testPlayWithArgsCallsLoadItem() {
public void testPlayEmptyArgsShowsHelpWhenNotPaused() {
when(audioPlayer.getPlayingTrack()).thenReturn(null);
- playerService.play(guild, member, null, textChannel, output);
+ musicService.play(guild, member, null, textChannel, output);
verify(output).onShowHelp();
}
From dc86f9a81fe91a93c3bdc782c7b9d060d79c9688 Mon Sep 17 00:00:00 2001
From: Arif Banai <6625454+arif-banai@users.noreply.github.com>
Date: Sun, 25 Jan 2026 22:11:35 -0500
Subject: [PATCH 21/24] Implement slash commands, refactor services, update
readme
- Updated README to note min Java version required - Java 25
- Introduced new slash command classes for admin functionalities, including `PrefixSlashCmd`, `QueuetypeSlashCmd`, and others, enhancing command management.
- Refactored existing commands to utilize the new `MusicService` for improved functionality and consistency.
- Added architectural documentation for better understanding of the system structure.
- Enhanced the `run_jmusicbot.sh` script with JVM options for better compatibility with Java 22+.
---
README.md | 47 +-
docs/ARCHITECTURE.md | 495 ++++++++++
scripts/run_jmusicbot.sh | 7 +-
.../jmusicbot/commands/v1/CommandFactory.java | 47 +-
.../commands/v1/TextOutputAdapters.java | 32 +
.../commands/v1/dj/ForceRemoveCmd.java | 52 +-
.../commands/v1/dj/ForceskipCmd.java | 21 +-
.../commands/v1/dj/MoveTrackCmd.java | 38 +-
.../jmusicbot/commands/v1/dj/PauseCmd.java | 19 +-
.../jmusicbot/commands/v1/dj/PlaynextCmd.java | 94 +-
.../jmusicbot/commands/v1/dj/RepeatCmd.java | 30 +-
.../jmusicbot/commands/v1/dj/SkiptoCmd.java | 31 +-
.../jmusicbot/commands/v1/dj/StopCmd.java | 15 +-
.../jmusicbot/commands/v1/dj/VolumeCmd.java | 41 +-
.../commands/v1/music/NowPlayingCmd.java | 21 +-
.../jmusicbot/commands/v1/music/QueueCmd.java | 116 ++-
.../commands/v1/music/RemoveCmd.java | 69 +-
.../jmusicbot/commands/v1/music/SeekCmd.java | 60 +-
.../commands/v1/music/ShuffleCmd.java | 17 +-
.../jmusicbot/commands/v1/music/SkipCmd.java | 52 +-
.../commands/v2/AdminSlashCommand.java | 59 ++
.../commands/v2/admin/PrefixSlashCmd.java | 60 ++
.../commands/v2/admin/QueuetypeSlashCmd.java | 83 ++
.../commands/v2/admin/SetdjSlashCmd.java | 61 ++
.../commands/v2/admin/SettcSlashCmd.java | 69 ++
.../commands/v2/admin/SetvcSlashCmd.java | 70 ++
.../commands/v2/admin/SkipratioSlashCmd.java | 55 ++
.../commands/v2/dj/ForceremoveSlashCmd.java | 73 ++
.../commands/v2/dj/ForceskipSlashCmd.java | 53 ++
.../commands/v2/dj/MovetrackSlashCmd.java | 81 ++
.../commands/v2/dj/PlaynextSlashCmd.java | 62 ++
.../commands/v2/dj/RepeatSlashCmd.java | 85 ++
.../commands/v2/dj/SkiptoSlashCmd.java | 67 ++
.../commands/v2/music/PlaylistsSlashCmd.java | 71 ++
.../commands/v2/music/QueueSlashCmd.java | 108 +++
.../commands/v2/music/RemoveSlashCmd.java | 80 ++
.../commands/v2/music/SearchSlashCmd.java | 201 ++++
.../commands/v2/music/SeekSlashCmd.java | 55 ++
.../commands/v2/music/ShuffleSlashCmd.java | 61 ++
.../commands/v2/music/SkipSlashCmd.java | 50 +
.../service/AudioLoadResultHandlers.java | 352 +++++++
.../jmusicbot/service/MusicService.java | 863 +++++++++++++-----
.../jmusicbot/service/SearchService.java | 27 +
43 files changed, 3366 insertions(+), 684 deletions(-)
create mode 100644 docs/ARCHITECTURE.md
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/AdminSlashCommand.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/PrefixSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/QueuetypeSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetdjSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SettcSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetvcSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SkipratioSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceremoveSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceskipSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/MovetrackSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/PlaynextSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/RepeatSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/SkiptoSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/PlaylistsSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/QueueSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/RemoveSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SeekSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/ShuffleSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SkipSlashCmd.java
create mode 100644 src/main/java/com/jagrosh/jmusicbot/service/AudioLoadResultHandlers.java
diff --git a/README.md b/README.md
index 621fe57bd..f30461e7e 100644
--- a/README.md
+++ b/README.md
@@ -16,13 +16,17 @@
A cross-platform Discord music bot with a clean interface, and that is easy to set up and run yourself!
-## ⚠️ Important Notice (Java 25)
+## ⚠️ Check your Java Version and other requirements
+
+This version of JMusicBot changes/updates various dependencies. To ensure your bot continues to function correctly, please note the following mandatory changes:
* **Java 25 Minimum:** The bot now requires **Java 25 or higher**. Please update your hosting environment (check `java -version`) before running the new JAR.
+* **LibDave/udpqueue:** You **must** install the required native libraries for your operating system. If you are using Docker, this is already handled for you.
* **Privileged Gateway Intents:** You **must** enable the **Message Content Intent** in your [Discord Developer Portal](https://discord.com/developers/applications).
* *Navigate to: Your Application > Bot > Privileged Gateway Intents > Toggle "Message Content Intent" to ON.*
* *Without this, the bot will not see your commands.*
+
[](https://jmusicbot.com/setup)
## Features
@@ -63,6 +67,43 @@ JMusicBot supports all sources and formats supported by [lavaplayer](https://git
## Setup
Please see the [Setup Page](https://jmusicbot.com/setup) to run this bot yourself!
+## Running Directly (Without Docker)
+
+When running JMusicBot directly (not in Docker), you need to use specific JVM flags for Java 22+.
+
+### Required JVM Flags
+
+Java 22 and later require the `--enable-native-access=ALL-UNNAMED` flag to load native libraries (JDave for Discord voice encryption, udpqueue for audio sending):
+
+```bash
+java -Dnogui=true --enable-native-access=ALL-UNNAMED -jar JMusicBot-0.6.2-All.jar
+```
+
+### Linux System Requirements
+
+On Debian/Ubuntu-based systems, you may need to install the following dependencies for native audio libraries:
+
+```bash
+# Install required native library dependencies
+sudo apt-get update
+sudo apt-get install -y libopus0 libsodium23
+```
+
+### Using the Run Script
+
+The included `scripts/run_jmusicbot.sh` script handles the JVM flags automatically:
+
+```bash
+chmod +x scripts/run_jmusicbot.sh
+./scripts/run_jmusicbot.sh
+```
+
+You can customize JVM options by setting the `JAVA_OPTS` environment variable:
+
+```bash
+JAVA_OPTS="--enable-native-access=ALL-UNNAMED -Xmx512m" ./scripts/run_jmusicbot.sh
+```
+
## Docker
JMusicBot can be run using Docker for easy deployment and management. Pre-built images are available from the GitHub Container Registry. The container is configured to run headless and automatically generate a default `config.txt` on first run.
@@ -123,6 +164,10 @@ services:
Check the [Docker Compose Example](docker-compose.example.yml) for more details.
+The Dockerfile uses a multi-stage build:
+- **Stage 1:** Builds the application with Maven (Java 25) - copies `pom.xml` first for better layer caching, then builds the shaded jar
+- **Stage 2:** Creates a minimal runtime image with `eclipse-temurin:25-jre-noble` - copies the built jar as `/app/app.jar` and sets up the entrypoint script
+
### Important Notes
- **Config Persistence:** The `/musicbot` volume **must** be mounted for your configuration to persist. The bot reads and writes `config.txt` from `/musicbot` (the container's working directory).
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 000000000..0b5075008
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,495 @@
+# JMusicBot Architectural Analysis
+
+> **Document Metadata**
+> | Field | Value |
+> |-------|-------|
+> | Generated | 2026-01-25 |
+> | Branch | `slash-commands-v1` |
+> | Commit | `c0f0a21` ([c0f0a214a50121a5b66ba28917a57013d9cb6a5f](https://github.com/arif-banai/MusicBot/commit/c0f0a214a50121a5b66ba28917a57013d9cb6a5f)) |
+> | Version | `v0.6.1-21-gc0f0a21` (0.6.2-slash-commands) |
+> | Base Commit Message | "Refactor music service and command structure" |
+
+---
+
+## Executive Summary
+
+JMusicBot is a cross-platform Discord music bot built with Java 25, using JDA (Java Discord API) for Discord integration and Lavaplayer for audio streaming. The codebase follows a **layered architecture** with clear separation between presentation (commands), business logic (services), and infrastructure (audio/settings).
+
+---
+
+## High-Level Architecture Diagram
+
+```mermaid
+flowchart TB
+ subgraph presentation [Presentation Layer]
+ TextCmd[Text Commands v1]
+ SlashCmd[Slash Commands v2]
+ GUI[GUI Panel]
+ end
+
+ subgraph business [Business Logic Layer]
+ MusicSvc[MusicService]
+ SearchSvc[SearchService]
+ Validator[MusicCommandValidator]
+ end
+
+ subgraph infrastructure [Infrastructure Layer]
+ subgraph audio [Audio Subsystem]
+ PlayerMgr[PlayerManager]
+ AudioHandler[AudioHandler]
+ AudioSrc[AudioSource Managers]
+ end
+ subgraph persistence [Persistence]
+ SettingsMgr[SettingsManager]
+ PlaylistLoader[PlaylistLoader]
+ ConfigLoader[ConfigLoader]
+ end
+ subgraph queue [Queue System]
+ AbstractQueue[AbstractQueue]
+ LinearQueue[LinearQueue]
+ FairQueue[FairQueue]
+ end
+ end
+
+ subgraph external [External Dependencies]
+ JDA[JDA Discord API]
+ Lavaplayer[Lavaplayer]
+ YouTube[YouTube/SoundCloud/etc]
+ Discord[Discord Gateway]
+ end
+
+ TextCmd --> Validator
+ SlashCmd --> Validator
+ Validator --> MusicSvc
+ Validator --> SearchSvc
+
+ MusicSvc --> AudioHandler
+ SearchSvc --> PlayerMgr
+
+ AudioHandler --> AbstractQueue
+ AbstractQueue --> LinearQueue
+ AbstractQueue --> FairQueue
+
+ AudioHandler --> PlayerMgr
+ PlayerMgr --> AudioSrc
+ AudioSrc --> Lavaplayer
+
+ AudioHandler --> JDA
+ JDA --> Discord
+ Lavaplayer --> YouTube
+
+ MusicSvc --> SettingsMgr
+ GUI --> MusicSvc
+```
+
+---
+
+## Package Overview
+
+### 1. Root Package: `com.jagrosh.jmusicbot`
+
+| Class | Role |
+|-------|------|
+| `JMusicBot` | Application entry point, bootstraps all components |
+| `Bot` | Central container holding references to all subsystems |
+| `BotConfig` | Configuration loading and access facade |
+| `DiscordService` | JDA initialization and Discord connection setup |
+| `Listener` | Discord event handler (ready, voice updates, buttons) |
+
+### 2. Commands Package: `commands.v1` and `commands.v2`
+
+**Purpose**: Presentation layer handling user input via text commands (v1) or slash commands (v2).
+
+**Hierarchy**:
+```
+Command (JDA-Utils) SlashCommand (JDA-Utils)
+ ├── MusicCommand ├── MusicSlashCommand
+ │ └── DJCommand │ └── DJSlashCommand
+ ├── AdminCommand └── AdminSlashCommand
+ └── OwnerCommand
+```
+
+**Key Classes**:
+- `MusicCommand` / `MusicSlashCommand`: Base for music playback commands
+- `DJCommand` / `DJSlashCommand`: Requires DJ role or permissions
+- `AdminCommand` / `AdminSlashCommand`: Requires MANAGE_SERVER
+- `CommandFactory`: Creates and registers all commands
+- `SlashCommandRegistry`: Handles Discord slash command upsert with hash-based change detection
+
+**Adapters**: `TextOutputAdapters` and `SlashOutputAdapters` implement `OutputAdapter` interface for command-agnostic responses.
+
+### 3. Service Package: `service`
+
+**Purpose**: Business logic layer encapsulating music operations.
+
+| Service | Responsibilities |
+|---------|------------------|
+| `MusicService` | Player control, queue management, volume, repeat, seek, skip |
+| `SearchService` | Track/playlist search with callback handling |
+| `AudioLoadResultHandlers` | Async handlers for Lavaplayer load results |
+
+**Key Design**: Services accept `OutputAdapter` to remain command-type agnostic.
+
+### 4. Audio Package: `audio`
+
+**Purpose**: Audio playback and Discord voice integration.
+
+| Class | Role |
+|-------|------|
+| `PlayerManager` | Extends Lavaplayer's `DefaultAudioPlayerManager`, registers sources |
+| `AudioHandler` | Per-guild audio handler, implements JDA's `AudioSendHandler` |
+| `AudioSource` | Enum of audio sources (YouTube, SoundCloud, etc.) with registration logic |
+| `QueuedTrack` | Wraps `AudioTrack` with request metadata |
+| `RequestMetadata` | Stores requester info (user, channel, query) |
+| `NowPlayingHandler` | Manages "now playing" message updates |
+| `AloneInVoiceHandler` | Auto-disconnect when alone in voice channel |
+| `TransformativeAudioSourceManager` | URL transformation via regex/CSS selectors |
+
+### 5. Queue Package: `queue`
+
+**Purpose**: Pluggable queue implementations.
+
+```mermaid
+classDiagram
+ class Queueable {
+ <>
+ +getIdentifier() long
+ }
+
+ class AbstractQueue~T~ {
+ <>
+ -list: List~T~
+ -history: HistoryQueue~T~
+ +add(T item)* int
+ +pull() T
+ +shuffle(int startIndex)
+ +moveItem(int from, int to)
+ }
+
+ class LinearQueue~T~ {
+ +add(T item) int
+ }
+
+ class FairQueue~T~ {
+ +add(T item) int
+ }
+
+ class HistoryQueue~T~ {
+ -history: LinkedList~T~
+ -maxSize: int
+ +add(T item)
+ +pop() T
+ }
+
+ class QueuedTrack {
+ -track: AudioTrack
+ -metadata: RequestMetadata
+ +getIdentifier() long
+ }
+
+ Queueable <|.. QueuedTrack
+ AbstractQueue <|-- LinearQueue
+ AbstractQueue <|-- FairQueue
+ AbstractQueue o-- HistoryQueue
+ AbstractQueue o-- Queueable
+```
+
+**Strategy Pattern**: `QueueType` enum holds `QueueSupplier` references for runtime strategy selection.
+
+### 6. Settings Package: `settings`
+
+**Purpose**: Per-guild configuration persistence.
+
+| Class | Role |
+|-------|------|
+| `Settings` | Per-guild settings POJO (volume, repeat, queue type, prefix, etc.) |
+| `SettingsManager` | CRUD operations, JSON persistence (`serversettings.json`) |
+| `QueueType` | Enum: LINEAR, FAIR with queue factory |
+| `RepeatMode` | Enum: OFF, ALL, SINGLE |
+
+**Persistence**: Jackson JSON serialization, auto-save on mutation.
+
+### 7. Config Package: `config`
+
+**Purpose**: Application configuration with versioned migration support.
+
+| Subpackage | Responsibility |
+|------------|----------------|
+| `loader` | Loads and merges configs |
+| `io` | File I/O operations |
+| `migration` | Version migrations (0→1→...) |
+| `model` | `ConfigOption` enum with type-safe accessors |
+| `diagnostics` | Detects missing/deprecated keys |
+| `validation` | Validates required fields (token, owner) |
+| `render` | Generates updated config preserving template |
+| `update` | Manages config file updates with backups |
+
+---
+
+## UML Class Diagram: Core Classes
+
+```mermaid
+classDiagram
+ class Bot {
+ -jda: JDA
+ -config: BotConfig
+ -settingsManager: SettingsManager
+ -playerManager: PlayerManager
+ -musicService: MusicService
+ -searchService: SearchService
+ +shutdown()
+ }
+
+ class MusicService {
+ -bot: Bot
+ +play(Guild, Member, String, TextChannel, OutputAdapter)
+ +skip(Guild, Member, OutputAdapter)
+ +pause(Guild, Member, OutputAdapter)
+ +stop(Guild, Member, OutputAdapter)
+ +setVolume(Guild, int)
+ +addTrackToQueue(Guild, Member, AudioTrack, String, TextChannel)
+ }
+
+ class AudioHandler {
+ -audioPlayer: AudioPlayer
+ -queue: AbstractQueue
+ -manager: PlayerManager
+ +addTrack(QueuedTrack)
+ +addTrackToFront(QueuedTrack)
+ +stopAndClear()
+ +provide20MsAudio() ByteBuffer
+ }
+
+ class PlayerManager {
+ -bot: Bot
+ +init()
+ +setUpHandler(Guild) AudioHandler
+ +loadItemOrdered(Object, String, AudioLoadResultHandler)
+ }
+
+ class MusicCommand {
+ <>
+ #musicService: MusicService
+ #bot: Bot
+ +execute(CommandEvent)
+ #doCommand(CommandEvent)*
+ }
+
+ class MusicSlashCommand {
+ <>
+ #musicService: MusicService
+ #bot: Bot
+ +execute(SlashCommandEvent)
+ #doCommand(SlashCommandEvent)*
+ }
+
+ class OutputAdapter {
+ <>
+ +reply(String)
+ +replyWarning(String)
+ +replyError(String)
+ +replySuccess(String)
+ }
+
+ Bot --> MusicService
+ Bot --> PlayerManager
+ Bot --> SettingsManager
+ MusicService --> AudioHandler
+ MusicService --> OutputAdapter
+ AudioHandler --> PlayerManager
+ MusicCommand --> MusicService
+ MusicSlashCommand --> MusicService
+```
+
+---
+
+## Design Patterns Identified
+
+| Pattern | Location | Purpose |
+|---------|----------|---------|
+| **Strategy** | `QueueType` + `AbstractQueue` | Pluggable queue algorithms (Linear/Fair) |
+| **Template Method** | `MusicCommand.execute()` → `doCommand()` | Common validation, subclass-specific logic |
+| **Adapter** | `OutputAdapter` implementations | Unified output interface for text/slash commands |
+| **Factory** | `CommandFactory`, `QueueSupplier` | Object creation encapsulation |
+| **Observer** | `AudioEventAdapter` in `AudioHandler` | React to Lavaplayer track events |
+| **Singleton-like** | `Bot`, `SettingsManager` | Single instance per application |
+| **Registry** | `ConfigMigration`, `SlashCommandRegistry` | Centralized registration |
+| **Facade** | `BotConfig`, `MusicService` | Simplified interface to complex subsystems |
+| **Command** | JDA-Utils command framework | Encapsulated user requests |
+| **Builder** | JDA's message builders | Fluent message construction |
+
+---
+
+## Data Flow: Playing a Track
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant SlashCmd as PlaySlashCmd
+ participant Validator as MusicCommandValidator
+ participant MusicSvc as MusicService
+ participant PlayerMgr as PlayerManager
+ participant AudioSrc as AudioSourceManager
+ participant Handler as AudioHandler
+ participant JDA
+ participant Discord
+
+ User->>SlashCmd: /play "song name"
+ SlashCmd->>Validator: validate(event)
+ Validator-->>SlashCmd: valid
+ SlashCmd->>MusicSvc: play(guild, member, query, channel, adapter)
+ MusicSvc->>PlayerMgr: loadItemOrdered(guild, query, handler)
+ PlayerMgr->>AudioSrc: loadItem(query)
+ AudioSrc-->>PlayerMgr: AudioTrack
+ PlayerMgr-->>MusicSvc: trackLoaded callback
+ MusicSvc->>Handler: addTrack(QueuedTrack)
+ Handler->>Handler: audioPlayer.playTrack()
+ loop Every 20ms
+ JDA->>Handler: provide20MsAudio()
+ Handler-->>JDA: ByteBuffer (Opus)
+ JDA->>Discord: Send audio packet
+ end
+ MusicSvc-->>SlashCmd: success message
+ SlashCmd-->>User: "Added song to queue"
+```
+
+---
+
+## Implicit Architectural Decisions
+
+### 1. Layered Architecture
+- **Presentation**: Commands (v1/v2) handle user input
+- **Business Logic**: Services contain domain operations
+- **Infrastructure**: Audio, persistence, Discord integration
+
+### 2. Command Version Separation
+- `v1` = Text commands (legacy, prefix-based)
+- `v2` = Slash commands (modern Discord UI)
+- Both share `MusicService` and `OutputAdapter` abstraction
+
+### 3. Per-Guild Isolation
+- Each guild has its own `AudioHandler`, `Settings`, and queue
+- No cross-guild state leakage
+
+### 4. Configuration Versioning
+- `meta.configVersion` enables forward-compatible migrations
+- Backups created before updates
+
+### 5. Event-Driven Audio
+- Lavaplayer events drive track lifecycle
+- `AudioHandler` extends `AudioEventAdapter` for hooks
+
+### 6. Deferred Initialization
+- `PlayerManager.init()` called after GUI to ensure logging is ready
+- Audio sources registered at runtime based on config
+
+---
+
+## Improvement Opportunities
+
+### 1. Reduce Service Coupling
+**Issue**: `MusicService` (1192 lines) has too many responsibilities.
+**Solution**: Extract sub-services:
+- `QueueService` for queue operations
+- `PlayerControlService` for play/pause/stop/seek
+- `VolumeService` for volume management
+
+### 2. Complete v2 Command Migration
+**Issue**: v2 (slash commands) lacks Owner and General command categories.
+**Solution**: Migrate remaining commands to slash versions for consistent UX.
+
+### 3. Standardize Result Objects
+**Issue**: Some methods return raw values, others return result objects.
+**Solution**: Consistently use result objects (e.g., `OperationResult`) for all service methods.
+
+### 4. Extract Audio Source Registration
+**Issue**: `AudioSource` enum contains complex registration logic.
+**Solution**: Use a dedicated `AudioSourceRegistry` class with pluggable source providers.
+
+### 5. Add Dependency Injection Framework
+**Issue**: Manual dependency wiring in `Bot` constructor.
+**Solution**: Consider lightweight DI (Guice, Dagger) for cleaner component management and testing.
+
+### 6. Improve Test Coverage
+**Issue**: Limited unit test infrastructure.
+**Solution**: Add mocking for Discord/audio layers, increase coverage of service layer.
+
+### 7. Configuration Hot-Reload
+**Issue**: Config changes require restart.
+**Solution**: Add file watcher for hot-reloading non-critical settings.
+
+### 8. Unified Error Handling
+**Issue**: Error handling varies across commands.
+**Solution**: Create `CommandException` hierarchy with consistent handling in base classes.
+
+---
+
+## Technology Stack
+
+| Component | Technology |
+|-----------|------------|
+| Language | Java 25 |
+| Build | Maven 3.8+ |
+| Discord API | JDA 6.3.0 |
+| Command Framework | JDA-Chewtils 2.2.1 |
+| Audio Playback | Lavaplayer 2.2.6 |
+| YouTube Support | Lavalink YouTube Source 1.16.0 |
+| Voice Protocol | JDave (DAVE protocol) |
+| Native Audio | udpqueue (UDP audio queuing) |
+| Configuration | Typesafe Config (HOCON) |
+| Persistence | Jackson JSON |
+| Logging | Logback + SLF4J |
+| HTML Parsing | JSoup |
+| Testing | JUnit 5, Mockito, Hamcrest |
+
+---
+
+## Startup Flow
+
+```
+main()
+ └─> startBot()
+ ├─> Create UserInteraction (Prompt)
+ ├─> Redirect console streams (if GUI)
+ ├─> Acquire instance lock
+ ├─> Check versions
+ ├─> Load BotConfig
+ │ └─> Validate token & owner
+ ├─> Set log level
+ ├─> Create EventWaiter
+ ├─> Create SettingsManager
+ ├─> Create Bot instance
+ │ ├─> Initialize PlaylistLoader
+ │ ├─> Initialize PlayerManager (not init() yet)
+ │ ├─> Initialize NowPlayingHandler
+ │ ├─> Initialize AloneInVoiceHandler
+ │ ├─> Initialize MusicService
+ │ ├─> Initialize SearchService
+ │ └─> Initialize YoutubeOauth2TokenHandler
+ ├─> Initialize GUI (if enabled)
+ ├─> Create CommandClient (via CommandFactory)
+ ├─> Initialize PlayerManager (register audio sources)
+ └─> Create JDA & connect to Discord
+ └─> Register Listener for events
+```
+
+---
+
+ ## Document History
+
+This architectural analysis was generated using AI-assisted code analysis. To update this document after significant changes:
+
+1. Review the current codebase structure for new packages/classes
+2. Update diagrams to reflect new components or data flows
+3. Add new design patterns if introduced
+4. Update the metadata table at the top with the new commit info
+
+**Regeneration command** (for reference):
+```bash
+git log -1 --format="%H %h %s %ai"
+git describe --tags --always
+```
+
+---
+
+*Last generated: 2026-01-25 | Commit: c0f0a21 | Branch: slash-commands-v1*
diff --git a/scripts/run_jmusicbot.sh b/scripts/run_jmusicbot.sh
index bbdaa909d..7c1d870d2 100644
--- a/scripts/run_jmusicbot.sh
+++ b/scripts/run_jmusicbot.sh
@@ -8,6 +8,10 @@ DOWNLOAD=true
# when you use the shutdown command
LOOP=true
+# JVM options for running JMusicBot
+# --enable-native-access=ALL-UNNAMED: Required for Java 22+ to load native libraries (jdave, udpqueue)
+JAVA_OPTS="${JAVA_OPTS:---enable-native-access=ALL-UNNAMED}"
+
download() {
if [ $DOWNLOAD = true ]; then
# First, check if the latest release is a pre-release
@@ -47,7 +51,8 @@ download() {
}
run() {
- java -Dnogui=true --enable-native-access=ALL-UNNAMED -jar $(ls -t JMusicBot* | head -1)
+ # shellcheck disable=SC2086
+ java -Dnogui=true $JAVA_OPTS -jar $(ls -t JMusicBot* | head -1)
}
while
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/CommandFactory.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/CommandFactory.java
index fb995a0f9..271b47985 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/CommandFactory.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/CommandFactory.java
@@ -12,11 +12,30 @@
import com.jagrosh.jmusicbot.commands.v1.general.SettingsCmd;
import com.jagrosh.jmusicbot.commands.v1.music.*;
import com.jagrosh.jmusicbot.commands.v1.owner.*;
+import com.jagrosh.jmusicbot.commands.v2.admin.PrefixSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.admin.QueuetypeSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.admin.SetdjSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.admin.SettcSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.admin.SetvcSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.admin.SkipratioSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.ForceskipSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.ForceremoveSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.MovetrackSlashCmd;
import com.jagrosh.jmusicbot.commands.v2.dj.PauseSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.PlaynextSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.RepeatSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.dj.SkiptoSlashCmd;
import com.jagrosh.jmusicbot.commands.v2.dj.StopSlashCmd;
import com.jagrosh.jmusicbot.commands.v2.dj.VolumeSlashCmd;
import com.jagrosh.jmusicbot.commands.v2.music.NowPlayingSlashCmd;
import com.jagrosh.jmusicbot.commands.v2.music.PlaySlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.PlaylistsSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.QueueSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.RemoveSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.SearchSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.SeekSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.ShuffleSlashCmd;
+import com.jagrosh.jmusicbot.commands.v2.music.SkipSlashCmd;
import com.jagrosh.jmusicbot.settings.SettingsManager;
import com.jagrosh.jmusicbot.utils.OtherUtil;
import net.dv8tion.jda.api.OnlineStatus;
@@ -80,11 +99,35 @@ public static CommandClient createCommandClient(BotConfig config, SettingsManage
new SetstatusCmd(bot),
new ShutdownCmd(bot)
).addSlashCommands(
- new PlaySlashCmd(bot),
+ // Music commands
new NowPlayingSlashCmd(bot),
+ new PlaySlashCmd(bot),
+ new PlaylistsSlashCmd(bot),
+ new QueueSlashCmd(bot),
+ new RemoveSlashCmd(bot),
+ new SearchSlashCmd(bot),
+ new SeekSlashCmd(bot),
+ new ShuffleSlashCmd(bot),
+ new SkipSlashCmd(bot),
+
+ // DJ commands
+ new ForceremoveSlashCmd(bot),
+ new ForceskipSlashCmd(bot),
+ new MovetrackSlashCmd(bot),
new PauseSlashCmd(bot),
+ new PlaynextSlashCmd(bot),
+ new RepeatSlashCmd(bot),
+ new SkiptoSlashCmd(bot),
new StopSlashCmd(bot),
- new VolumeSlashCmd(bot)
+ new VolumeSlashCmd(bot),
+
+ // Admin commands
+ new PrefixSlashCmd(bot),
+ new QueuetypeSlashCmd(bot),
+ new SetdjSlashCmd(bot),
+ new SettcSlashCmd(bot),
+ new SetvcSlashCmd(bot),
+ new SkipratioSlashCmd(bot)
).setManualUpsert(true);
if (config.useEval())
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/TextOutputAdapters.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/TextOutputAdapters.java
index 7d8de26f7..c1b9fca62 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/TextOutputAdapters.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/TextOutputAdapters.java
@@ -29,6 +29,38 @@ public final class TextOutputAdapters
{
private TextOutputAdapters() {} // Utility class
+ /**
+ * Simple OutputAdapter for direct CommandEvent replies.
+ * Used for commands that need basic reply functionality.
+ */
+ public static class SimpleOutputAdapter extends BaseOutputAdapter
+ {
+ private final CommandEvent event;
+
+ public SimpleOutputAdapter(CommandEvent event)
+ {
+ this.event = event;
+ }
+
+ @Override
+ public void replySuccess(String content)
+ {
+ event.replySuccess(content);
+ }
+
+ @Override
+ public void replyError(String content)
+ {
+ event.replyError(content);
+ }
+
+ @Override
+ public void replyWarning(String content)
+ {
+ event.replyWarning(content);
+ }
+ }
+
/**
* OutputAdapter for direct CommandEvent replies (before any loading message is sent).
* Used when no args are provided and we need to show help or handle empty input.
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceRemoveCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceRemoveCmd.java
index e3259a984..4a827ddc9 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceRemoveCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceRemoveCmd.java
@@ -19,8 +19,8 @@
import com.jagrosh.jdautilities.commons.utils.FinderUtil;
import com.jagrosh.jdautilities.menu.OrderedMenu;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
@@ -35,9 +35,12 @@
*/
public class ForceRemoveCmd extends DJCommand
{
+ private final MusicService musicService;
+
public ForceRemoveCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "forceremove";
this.help = "removes all entries by a user from the queue";
this.arguments = "";
@@ -56,65 +59,56 @@ public void doCommand(CommandEvent event)
return;
}
- AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
- if (handler.getQueue().isEmpty())
+ if (musicService.isQueueEmpty(event.getGuild()))
{
event.replyError("There is nothing in the queue!");
return;
}
-
- User target;
List found = FinderUtil.findMembers(event.getArgs(), event.getGuild());
- if(found.isEmpty())
+ if (found.isEmpty())
{
event.replyError("Unable to find the user!");
return;
}
- else if(found.size()>1)
+ else if (found.size() > 1)
{
OrderedMenu.Builder builder = new OrderedMenu.Builder();
- for(int i=0; i removeAllEntries(found.get(i-1).getUser(), event))
- .setText("Found multiple users:")
- .setColor(event.getSelfMember().getColor())
- .useNumbers()
- .setUsers(event.getAuthor())
- .useCancelButton(true)
- .setCancel((msg) -> {})
- .setEventWaiter(bot.getWaiter())
- .setTimeout(1, TimeUnit.MINUTES)
-
- .build().display(event.getChannel());
+ .setSelection((msg, i) -> removeAllEntries(found.get(i - 1).getUser(), event))
+ .setText("Found multiple users:")
+ .setColor(event.getSelfMember().getColor())
+ .useNumbers()
+ .setUsers(event.getAuthor())
+ .useCancelButton(true)
+ .setCancel((msg) -> {})
+ .setEventWaiter(bot.getWaiter())
+ .setTimeout(1, TimeUnit.MINUTES)
+ .build().display(event.getChannel());
return;
}
- else
- {
- target = found.get(0).getUser();
- }
-
- removeAllEntries(target, event);
+ removeAllEntries(found.get(0).getUser(), event);
}
private void removeAllEntries(User target, CommandEvent event)
{
- int count = ((AudioHandler) event.getGuild().getAudioManager().getSendingHandler()).getQueue().removeAll(target.getIdLong());
+ int count = musicService.removeAllTracksByUser(event.getGuild(), target.getIdLong());
if (count == 0)
{
- event.replyWarning("**"+target.getName()+"** doesn't have any songs in the queue!");
+ event.replyWarning("**" + target.getName() + "** doesn't have any songs in the queue!");
}
else
{
- event.replySuccess("Successfully removed `"+count+"` entries from "+FormatUtil.formatUsername(target)+".");
+ event.replySuccess("Successfully removed `" + count + "` entries from " + FormatUtil.formatUsername(target) + ".");
}
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceskipCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceskipCmd.java
index 3d539519c..df66a78ed 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceskipCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/ForceskipCmd.java
@@ -17,20 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
-import com.jagrosh.jmusicbot.utils.FormatUtil;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class ForceskipCmd extends DJCommand
+public class ForceskipCmd extends DJCommand
{
+ private final MusicService musicService;
+
public ForceskipCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "forceskip";
this.help = "skips the current song";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -38,12 +39,12 @@ public ForceskipCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- RequestMetadata rm = handler.getRequestMetadata();
- event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title
- +"** "+(rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)"));
- handler.getPlayer().stopTrack();
+ MusicService.ForceSkipResult result = musicService.forceSkip(event.getGuild());
+ if (result != null)
+ {
+ event.replySuccess("Skipped **" + result.trackTitle + "** " + result.requesterInfo);
+ }
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/MoveTrackCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/MoveTrackCmd.java
index 52d5ee759..e358046e7 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/MoveTrackCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/MoveTrackCmd.java
@@ -1,22 +1,21 @@
package com.jagrosh.jmusicbot.commands.v1.dj;
-
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
-import com.jagrosh.jmusicbot.queue.AbstractQueue;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
* Command that provides users the ability to move a track in the playlist.
*/
public class MoveTrackCmd extends DJCommand
{
+ private final MusicService musicService;
public MoveTrackCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "movetrack";
this.help = "move a track in the current queue to a different position";
this.arguments = " ";
@@ -31,7 +30,7 @@ public void doCommand(CommandEvent event)
int to;
String[] parts = event.getArgs().split("\\s+", 2);
- if(parts.length < 2)
+ if (parts.length < 2)
{
event.replyError("Please include two valid indexes.");
return;
@@ -39,7 +38,6 @@ public void doCommand(CommandEvent event)
try
{
- // Validate the args
from = Integer.parseInt(parts[0]);
to = Integer.parseInt(parts[1]);
}
@@ -55,31 +53,21 @@ public void doCommand(CommandEvent event)
return;
}
- // Validate that from and to are available
- AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
- AbstractQueue queue = handler.getQueue();
- if (isUnavailablePosition(queue, from))
+ if (!musicService.isValidQueuePosition(event.getGuild(), from))
{
- String reply = String.format("`%d` is not a valid position in the queue!", from);
- event.replyError(reply);
+ event.replyError("`" + from + "` is not a valid position in the queue!");
return;
}
- if (isUnavailablePosition(queue, to))
+ if (!musicService.isValidQueuePosition(event.getGuild(), to))
{
- String reply = String.format("`%d` is not a valid position in the queue!", to);
- event.replyError(reply);
+ event.replyError("`" + to + "` is not a valid position in the queue!");
return;
}
- // Move the track
- QueuedTrack track = queue.moveItem(from - 1, to - 1);
- String trackTitle = track.getTrack().getInfo().title;
- String reply = String.format("Moved **%s** from position `%d` to `%d`.", trackTitle, from, to);
- event.replySuccess(reply);
- }
-
- private static boolean isUnavailablePosition(AbstractQueue queue, int position)
- {
- return (position < 1 || position > queue.size());
+ String trackTitle = musicService.moveTrackPosition(event.getGuild(), from, to);
+ if (trackTitle != null)
+ {
+ event.replySuccess("Moved **" + trackTitle + "** from position `" + from + "` to `" + to + "`.");
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PauseCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PauseCmd.java
index b15aac16e..3101dba34 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PauseCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PauseCmd.java
@@ -17,18 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class PauseCmd extends DJCommand
+public class PauseCmd extends DJCommand
{
+ private final MusicService musicService;
+
public PauseCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "pause";
this.help = "pauses the current song";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -36,15 +39,15 @@ public PauseCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- if(handler.getPlayer().isPaused())
+ if (musicService.isPaused(event.getGuild()))
{
- event.replyWarning("The player is already paused! Use `"+event.getClient().getPrefix()+"play` to unpause!");
+ event.replyWarning("The player is already paused! Use `" + event.getClient().getPrefix() + "play` to unpause!");
return;
}
- handler.getPlayer().setPaused(true);
- event.replySuccess("Paused **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**. Type `"+event.getClient().getPrefix()+"play` to unpause!");
+
+ String trackTitle = musicService.setPaused(event.getGuild(), true);
+ event.replySuccess("Paused **" + trackTitle + "**. Type `" + event.getClient().getPrefix() + "play` to unpause!");
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PlaynextCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PlaynextCmd.java
index 87647d61e..fc45ead75 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PlaynextCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/PlaynextCmd.java
@@ -17,17 +17,9 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.QueuedTrack;
-import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
-import com.jagrosh.jmusicbot.utils.FormatUtil;
-import com.jagrosh.jmusicbot.utils.TimeUtil;
-import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
-import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
-import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
-import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import net.dv8tion.jda.api.entities.Message;
+import com.jagrosh.jmusicbot.commands.v1.TextOutputAdapters.MessageEditOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
@@ -36,10 +28,12 @@
public class PlaynextCmd extends DJCommand
{
private final String loadingEmoji;
-
+ private final MusicService musicService;
+
public PlaynextCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.loadingEmoji = bot.getConfig().getLoading();
this.name = "playnext";
this.arguments = "";
@@ -48,84 +42,22 @@ public PlaynextCmd(Bot bot)
this.beListening = true;
this.bePlaying = false;
}
-
+
@Override
public void doCommand(CommandEvent event)
{
- if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
+ if (event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty())
{
event.replyWarning("Please include a song title or URL!");
return;
}
- String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
- ? event.getArgs().substring(1,event.getArgs().length()-1)
- : event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs();
- event.reply(loadingEmoji+" Loading... `["+args+"]`", m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), args, new ResultHandler(m,event,false)));
- }
-
- private class ResultHandler implements AudioLoadResultHandler
- {
- private final Message m;
- private final CommandEvent event;
- private final boolean ytsearch;
-
- private ResultHandler(Message m, CommandEvent event, boolean ytsearch)
- {
- this.m = m;
- this.event = event;
- this.ytsearch = ytsearch;
- }
-
- private void loadSingle(AudioTrack track)
- {
- if(bot.getConfig().isTooLong(track))
- {
- m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `"
- + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue();
- return;
- }
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- int pos = handler.addTrackToFront(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1;
- String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title
- +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos));
- m.editMessage(addMsg).queue();
- }
-
- @Override
- public void trackLoaded(AudioTrack track)
- {
- loadSingle(track);
- }
- @Override
- public void playlistLoaded(AudioPlaylist playlist)
- {
- AudioTrack single;
- if(playlist.getTracks().size()==1 || playlist.isSearchResult())
- single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
- else if (playlist.getSelectedTrack()!=null)
- single = playlist.getSelectedTrack();
- else
- single = playlist.getTracks().get(0);
- loadSingle(single);
- }
-
- @Override
- public void noMatches()
- {
- if(ytsearch)
- m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue();
- else
- bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true));
- }
+ String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">")
+ ? event.getArgs().substring(1, event.getArgs().length() - 1)
+ : event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs();
- @Override
- public void loadFailed(FriendlyException throwable)
- {
- if(throwable.severity==FriendlyException.Severity.COMMON)
- m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue();
- else
- m.editMessage(event.getClient().getError()+" Error loading track.").queue();
- }
+ event.reply(loadingEmoji + " Loading... `[" + args + "]`", m ->
+ musicService.playNext(event.getGuild(), event.getMember(), args, event.getTextChannel(),
+ new MessageEditOutputAdapter(m)));
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/RepeatCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/RepeatCmd.java
index d8a4d5d42..7f41bfd04 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/RepeatCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/RepeatCmd.java
@@ -18,8 +18,8 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
import com.jagrosh.jmusicbot.settings.RepeatMode;
-import com.jagrosh.jmusicbot.settings.Settings;
/**
*
@@ -27,39 +27,40 @@
*/
public class RepeatCmd extends DJCommand
{
+ private final MusicService musicService;
+
public RepeatCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "repeat";
this.help = "re-adds music to the queue when finished";
this.arguments = "[off|all|single]";
this.aliases = bot.getConfig().getAliases(this.name);
this.guildOnly = true;
}
-
+
// override musiccommand's execute because we don't actually care where this is used
@Override
- protected void execute(CommandEvent event)
+ protected void execute(CommandEvent event)
{
String args = event.getArgs();
RepeatMode value;
- Settings settings = event.getClient().getSettingsFor(event.getGuild());
- if(args.isEmpty())
+ RepeatMode currentMode = musicService.getRepeatMode(event.getGuild());
+
+ if (args.isEmpty())
{
- if(settings.getRepeatMode() == RepeatMode.OFF)
- value = RepeatMode.ALL;
- else
- value = RepeatMode.OFF;
+ value = (currentMode == RepeatMode.OFF) ? RepeatMode.ALL : RepeatMode.OFF;
}
- else if(args.equalsIgnoreCase("false") || args.equalsIgnoreCase("off"))
+ else if (args.equalsIgnoreCase("false") || args.equalsIgnoreCase("off"))
{
value = RepeatMode.OFF;
}
- else if(args.equalsIgnoreCase("true") || args.equalsIgnoreCase("on") || args.equalsIgnoreCase("all"))
+ else if (args.equalsIgnoreCase("true") || args.equalsIgnoreCase("on") || args.equalsIgnoreCase("all"))
{
value = RepeatMode.ALL;
}
- else if(args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single"))
+ else if (args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single"))
{
value = RepeatMode.SINGLE;
}
@@ -68,8 +69,9 @@ else if(args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single"))
event.replyError("Valid options are `off`, `all` or `single` (or leave empty to toggle between `off` and `all`)");
return;
}
- settings.setRepeatMode(value);
- event.replySuccess("Repeat mode is now `"+value.getUserFriendlyName()+"`");
+
+ musicService.setRepeatMode(event.getGuild(), value);
+ event.replySuccess("Repeat mode is now `" + value.getUserFriendlyName() + "`");
}
@Override
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/SkiptoCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/SkiptoCmd.java
index 8630b0b16..a4657c749 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/SkiptoCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/SkiptoCmd.java
@@ -17,18 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class SkiptoCmd extends DJCommand
+public class SkiptoCmd extends DJCommand
{
+ private final MusicService musicService;
+
public SkiptoCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "skipto";
this.help = "skips to the specified song";
this.arguments = "";
@@ -37,26 +40,30 @@ public SkiptoCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- int index = 0;
+ int index;
try
{
index = Integer.parseInt(event.getArgs());
}
- catch(NumberFormatException e)
+ catch (NumberFormatException e)
{
- event.reply(event.getClient().getError()+" `"+event.getArgs()+"` is not a valid integer!");
+ event.replyError("`" + event.getArgs() + "` is not a valid integer!");
return;
}
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- if(index<1 || index>handler.getQueue().size())
+
+ int queueSize = musicService.getQueueSize(event.getGuild());
+ if (index < 1 || index > queueSize)
{
- event.reply(event.getClient().getError()+" Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
+ event.replyError("Position must be a valid integer between 1 and " + queueSize + "!");
return;
}
- handler.getQueue().skip(index-1);
- event.reply(event.getClient().getSuccess()+" Skipped to **"+handler.getQueue().get(0).getTrack().getInfo().title+"**");
- handler.getPlayer().stopTrack();
+
+ String trackTitle = musicService.skipToPosition(event.getGuild(), index);
+ if (trackTitle != null)
+ {
+ event.replySuccess("Skipped to **" + trackTitle + "**");
+ }
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/StopCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/StopCmd.java
index 354bbedf4..d055a120f 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/StopCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/StopCmd.java
@@ -17,18 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class StopCmd extends DJCommand
+public class StopCmd extends DJCommand
{
+ private final MusicService musicService;
+
public StopCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "stop";
this.help = "stops the current song and clears the queue";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -36,11 +39,9 @@ public StopCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- handler.stopAndClear();
- event.getGuild().getAudioManager().closeAudioConnection();
- event.reply(event.getClient().getSuccess()+" The player has stopped and the queue has been cleared.");
+ musicService.stopAndClear(event.getGuild());
+ event.replySuccess("The player has stopped and the queue has been cleared.");
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/VolumeCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/VolumeCmd.java
index 50ca3c83a..99f47653d 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/VolumeCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/dj/VolumeCmd.java
@@ -17,9 +17,8 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.DJCommand;
-import com.jagrosh.jmusicbot.settings.Settings;
+import com.jagrosh.jmusicbot.service.MusicService;
import com.jagrosh.jmusicbot.utils.FormatUtil;
/**
@@ -28,9 +27,12 @@
*/
public class VolumeCmd extends DJCommand
{
+ private final MusicService musicService;
+
public VolumeCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "volume";
this.aliases = bot.getConfig().getAliases(this.name);
this.help = "sets or shows volume";
@@ -40,30 +42,33 @@ public VolumeCmd(Bot bot)
@Override
public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- Settings settings = event.getClient().getSettingsFor(event.getGuild());
- int volume = handler.getPlayer().getVolume();
- if(event.getArgs().isEmpty())
+ int currentVolume = musicService.getVolume(event.getGuild());
+
+ if (event.getArgs().isEmpty())
{
- event.reply(FormatUtil.volumeIcon(volume)+" Current volume is `"+volume+"`");
+ event.reply(FormatUtil.volumeIcon(currentVolume) + " Current volume is `" + currentVolume + "`");
}
else
{
- int nvolume;
- try{
- nvolume = Integer.parseInt(event.getArgs());
- }catch(NumberFormatException e){
- nvolume = -1;
+ int newVolume;
+ try
+ {
+ newVolume = Integer.parseInt(event.getArgs());
+ }
+ catch (NumberFormatException e)
+ {
+ newVolume = -1;
+ }
+
+ MusicService.VolumeResult result = musicService.setVolume(event.getGuild(), newVolume);
+ if (result == null)
+ {
+ event.replyError("Volume must be a valid integer between 0 and 150!");
}
- if(nvolume<0 || nvolume>150)
- event.reply(event.getClient().getError()+" Volume must be a valid integer between 0 and 150!");
else
{
- handler.getPlayer().setVolume(nvolume);
- settings.setVolume(nvolume);
- event.reply(FormatUtil.volumeIcon(nvolume)+" Volume changed from `"+volume+"` to `"+nvolume+"`");
+ event.reply(FormatUtil.volumeIcon(result.newVolume) + " Volume changed from `" + result.oldVolume + "` to `" + result.newVolume + "`");
}
}
}
-
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/NowPlayingCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/NowPlayingCmd.java
index b9113889a..18742cc12 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/NowPlayingCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/NowPlayingCmd.java
@@ -17,10 +17,9 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.utils.messages.MessageCreateData;
/**
*
@@ -28,9 +27,12 @@
*/
public class NowPlayingCmd extends MusicCommand
{
+ private final MusicService musicService;
+
public NowPlayingCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "nowplaying";
this.help = "shows the song that is currently playing";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -38,25 +40,24 @@ public NowPlayingCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
+ MusicService.NowPlayingInfo info = musicService.getNowPlayingInfo(event.getGuild(), event.getJDA());
- if(handler == null)
+ if (info == null)
{
- event.reply(event.getClient().getWarning() + " There is no music playing in this server.");
+ event.replyWarning("There is no music playing in this server.");
return;
}
- MessageCreateData nowPlayingMsg = handler.getNowPlaying(event.getJDA());
- if(nowPlayingMsg==null)
+ if (info.nowPlayingMessage == null || !info.isPlaying)
{
- event.reply(handler.getNoMusicPlaying(event.getJDA()));
+ event.reply(info.noMusicMessage);
bot.getNowplayingHandler().clearLastNPMessage(event.getGuild());
}
else
{
- event.reply(nowPlayingMsg, msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
+ event.reply(info.nowPlayingMessage, msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
}
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/QueueCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/QueueCmd.java
index 99e1055fa..6745be1c5 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/QueueCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/QueueCmd.java
@@ -18,54 +18,52 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jdautilities.menu.Paginator;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
-import com.jagrosh.jmusicbot.settings.QueueType;
-import com.jagrosh.jmusicbot.settings.RepeatMode;
-import com.jagrosh.jmusicbot.settings.Settings;
-import com.jagrosh.jmusicbot.utils.FormatUtil;
-import com.jagrosh.jmusicbot.utils.TimeUtil;
+import com.jagrosh.jmusicbot.service.MusicService;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.exceptions.PermissionException;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
-import java.util.List;
import java.util.concurrent.TimeUnit;
/**
*
* @author John Grosh
*/
-public class QueueCmd extends MusicCommand
+public class QueueCmd extends MusicCommand
{
private final Paginator.Builder builder;
-
+ private final MusicService musicService;
+
public QueueCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "queue";
this.help = "shows the current queue";
this.arguments = "[pagenum]";
this.aliases = bot.getConfig().getAliases(this.name);
this.bePlaying = true;
- this.botPermissions = new Permission[]{Permission.MESSAGE_ADD_REACTION,Permission.MESSAGE_EMBED_LINKS};
+ this.botPermissions = new Permission[]{Permission.MESSAGE_ADD_REACTION, Permission.MESSAGE_EMBED_LINKS};
builder = new Paginator.Builder()
- .setColumns(1)
- .setFinalAction(m -> {
- try {
- m.clearReactions().queue();
- } catch(PermissionException ignore){
- // do nothing
- }})
- .setItemsPerPage(10)
- .waitOnSinglePage(false)
- .useNumberedItems(true)
- .showPageNumbers(true)
- .wrapPageEnds(true)
- .setEventWaiter(bot.getWaiter())
- .setTimeout(1, TimeUnit.MINUTES);
+ .setColumns(1)
+ .setFinalAction(m -> {
+ try
+ {
+ m.clearReactions().queue();
+ }
+ catch (PermissionException ignore)
+ {
+ }
+ })
+ .setItemsPerPage(10)
+ .waitOnSinglePage(false)
+ .useNumberedItems(true)
+ .showPageNumbers(true)
+ .wrapPageEnds(true)
+ .setEventWaiter(bot.getWaiter())
+ .setTimeout(1, TimeUnit.MINUTES);
}
@Override
@@ -76,51 +74,39 @@ public void doCommand(CommandEvent event)
{
pagenum = Integer.parseInt(event.getArgs());
}
- catch(NumberFormatException ignore){}
- AudioHandler ah = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- List list = ah.getQueue().getList();
- if(list.isEmpty())
+ catch (NumberFormatException ignore)
{
- MessageCreateData nowp = ah.getNowPlaying(event.getJDA());
- MessageCreateData nonowp = ah.getNoMusicPlaying(event.getJDA());
- MessageCreateData built = new MessageCreateBuilder()
- .setContent(event.getClient().getWarning() + " There is no music in the queue!")
- .setEmbeds((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build();
- event.reply(built, m ->
- {
- if(nowp!=null)
- bot.getNowplayingHandler().setLastNPMessage(m);
- });
- return;
}
- String[] songs = new String[list.size()];
- long total = 0;
- for(int i=0; i
+ {
+ if (npInfo != null && npInfo.isPlaying)
+ bot.getNowplayingHandler().setLastNPMessage(m);
+ });
+ }
+ else
+ {
+ event.replyWarning("There is no music in the queue!");
+ }
+ return;
}
- Settings settings = event.getClient().getSettingsFor(event.getGuild());
- long fintotal = total;
- builder.setText((i1,i2) -> getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode(), settings.getQueueType()))
- .setItems(songs)
+
+ String successEmoji = event.getClient().getSuccess();
+ builder.setText((i1, i2) -> musicService.formatQueueTitle(queueInfo, successEmoji))
+ .setItems(queueInfo.tracks)
.setUsers(event.getAuthor())
- .setColor(event.getSelfMember().getColors().getPrimary())
- ;
+ .setColor(event.getSelfMember().getColors().getPrimary());
builder.build().paginate(event.getChannel(), pagenum);
}
-
- private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, RepeatMode repeatmode, QueueType queueType)
- {
- StringBuilder sb = new StringBuilder();
- if(ah.getPlayer().getPlayingTrack()!=null)
- {
- sb.append(ah.getStatusEmoji()).append(" **")
- .append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n");
- }
- return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength)
- .append(" entries | `").append(TimeUtil.formatTime(total)).append("` ")
- .append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`')
- .append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString());
- }
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/RemoveCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/RemoveCmd.java
index f294fc04e..b3e3daf9a 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/RemoveCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/RemoveCmd.java
@@ -17,22 +17,22 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.QueuedTrack;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
-import com.jagrosh.jmusicbot.settings.Settings;
-import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.entities.User;
+import com.jagrosh.jmusicbot.commands.v1.TextOutputAdapters.SimpleOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class RemoveCmd extends MusicCommand
+public class RemoveCmd extends MusicCommand
{
+ private final MusicService musicService;
+
public RemoveCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "remove";
this.help = "removes a song from the queue";
this.arguments = "";
@@ -42,59 +42,32 @@ public RemoveCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- if(handler.getQueue().isEmpty())
+ SimpleOutputAdapter output = new SimpleOutputAdapter(event);
+
+ if (musicService.isQueueEmpty(event.getGuild()))
{
- event.replyError("There is nothing in the queue!");
+ output.replyError("There is nothing in the queue!");
return;
}
- if(event.getArgs().equalsIgnoreCase("all"))
+
+ if (event.getArgs().equalsIgnoreCase("all"))
{
- int count = handler.getQueue().removeAll(event.getAuthor().getIdLong());
- if(count==0)
- event.replyWarning("You don't have any songs in the queue!");
- else
- event.replySuccess("Successfully removed your "+count+" entries.");
+ musicService.removeAllTracks(event.getGuild(), event.getMember(), output);
return;
}
+
int pos;
- try {
- pos = Integer.parseInt(event.getArgs());
- } catch(NumberFormatException e) {
- pos = 0;
- }
- if(pos<1 || pos>handler.getQueue().size())
+ try
{
- event.replyError("Position must be a valid integer between 1 and "+handler.getQueue().size()+"!");
- return;
- }
- Settings settings = event.getClient().getSettingsFor(event.getGuild());
- boolean isDJ = event.getMember().hasPermission(Permission.MANAGE_SERVER);
- if(!isDJ)
- isDJ = event.getMember().getRoles().contains(settings.getRole(event.getGuild()));
- QueuedTrack qt = handler.getQueue().get(pos-1);
- if(qt.getIdentifier()==event.getAuthor().getIdLong())
- {
- handler.getQueue().remove(pos-1);
- event.replySuccess("Removed **"+qt.getTrack().getInfo().title+"** from the queue");
- }
- else if(isDJ)
- {
- handler.getQueue().remove(pos-1);
- User u;
- try {
- u = event.getJDA().getUserById(qt.getIdentifier());
- } catch(Exception e) {
- u = null;
- }
- event.replySuccess("Removed **"+qt.getTrack().getInfo().title
- +"** from the queue (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")");
+ pos = Integer.parseInt(event.getArgs());
}
- else
+ catch (NumberFormatException e)
{
- event.replyError("You cannot remove **"+qt.getTrack().getInfo().title+"** because you didn't add it!");
+ pos = 0;
}
+
+ musicService.removeTrack(event.getGuild(), event.getMember(), pos, output);
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SeekCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SeekCmd.java
index a34398f91..8be0d0105 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SeekCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SeekCmd.java
@@ -17,26 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.RequestMetadata;
-import com.jagrosh.jmusicbot.commands.v1.DJCommand;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
-import com.jagrosh.jmusicbot.utils.TimeUtil;
-import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import com.jagrosh.jmusicbot.commands.v1.TextOutputAdapters.SimpleOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
* @author Whew., Inc.
*/
public class SeekCmd extends MusicCommand
{
- private final static Logger LOG = LoggerFactory.getLogger("Seeking");
-
+ private final MusicService musicService;
+
public SeekCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "seek";
this.help = "seeks the current song";
this.arguments = "[+ | -] |<0h0m0s | 0m0s | 0s>";
@@ -48,49 +43,6 @@ public SeekCmd(Bot bot)
@Override
public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
- AudioTrack playingTrack = handler.getPlayer().getPlayingTrack();
- if (!playingTrack.isSeekable())
- {
- event.replyError("This track is not seekable.");
- return;
- }
-
-
- if (!DJCommand.checkDJPermission(event) && playingTrack.getUserData(RequestMetadata.class).getOwner() != event.getAuthor().getIdLong())
- {
- event.replyError("You cannot seek **" + playingTrack.getInfo().title + "** because you didn't add it!");
- return;
- }
-
- String args = event.getArgs();
- TimeUtil.SeekTime seekTime = TimeUtil.parseTime(args);
- if (seekTime == null)
- {
- event.replyError("Invalid seek! Expected format: " + arguments + "\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`");
- return;
- }
-
- long currentPosition = playingTrack.getPosition();
- long trackDuration = playingTrack.getDuration();
-
- long seekMilliseconds = seekTime.relative ? currentPosition + seekTime.milliseconds : seekTime.milliseconds;
- if (seekMilliseconds > trackDuration)
- {
- event.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!");
- return;
- }
-
- try
- {
- playingTrack.setPosition(seekMilliseconds);
- }
- catch (Exception e)
- {
- event.replyError("An error occurred while trying to seek: " + e.getMessage());
- LOG.warn("Failed to seek track " + playingTrack.getIdentifier(), e);
- return;
- }
- event.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(playingTrack.getDuration()) + "`!");
+ musicService.seek(event.getGuild(), event.getMember(), event.getArgs(), new SimpleOutputAdapter(event));
}
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/ShuffleCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/ShuffleCmd.java
index fbc36b65f..9f19f8866 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/ShuffleCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/ShuffleCmd.java
@@ -17,18 +17,21 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class ShuffleCmd extends MusicCommand
+public class ShuffleCmd extends MusicCommand
{
+ private final MusicService musicService;
+
public ShuffleCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "shuffle";
this.help = "shuffles songs you have added";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -37,11 +40,10 @@ public ShuffleCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- int s = handler.getQueue().shuffle(event.getAuthor().getIdLong());
- switch (s)
+ int shuffled = musicService.shuffleUserTracks(event.getGuild(), event.getAuthor().getIdLong());
+ switch (shuffled)
{
case 0:
event.replyError("You don't have any music in the queue to shuffle!");
@@ -50,9 +52,8 @@ public void doCommand(CommandEvent event)
event.replyWarning("You only have one song in the queue!");
break;
default:
- event.replySuccess("You successfully shuffled your "+s+" entries.");
+ event.replySuccess("You successfully shuffled your " + shuffled + " entries.");
break;
}
}
-
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SkipCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SkipCmd.java
index 152a70ae5..34f444f1e 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SkipCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v1/music/SkipCmd.java
@@ -17,20 +17,22 @@
import com.jagrosh.jdautilities.command.CommandEvent;
import com.jagrosh.jmusicbot.Bot;
-import com.jagrosh.jmusicbot.audio.AudioHandler;
-import com.jagrosh.jmusicbot.audio.RequestMetadata;
import com.jagrosh.jmusicbot.commands.v1.MusicCommand;
-import com.jagrosh.jmusicbot.utils.FormatUtil;
+import com.jagrosh.jmusicbot.commands.v1.TextOutputAdapters.SimpleOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
/**
*
* @author John Grosh
*/
-public class SkipCmd extends MusicCommand
+public class SkipCmd extends MusicCommand
{
+ private final MusicService musicService;
+
public SkipCmd(Bot bot)
{
super(bot);
+ this.musicService = bot.getMusicService();
this.name = "skip";
this.help = "votes to skip the current song";
this.aliases = bot.getConfig().getAliases(this.name);
@@ -39,43 +41,11 @@ public SkipCmd(Bot bot)
}
@Override
- public void doCommand(CommandEvent event)
+ public void doCommand(CommandEvent event)
{
- AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler();
- RequestMetadata rm = handler.getRequestMetadata();
- double skipRatio = bot.getSettingsManager().getSettings(event.getGuild()).getSkipRatio();
- if(skipRatio == -1) {
- skipRatio = bot.getConfig().getSkipRatio();
- }
- if(event.getAuthor().getIdLong() == rm.getOwner() || skipRatio == 0)
- {
- event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**");
- handler.getPlayer().stopTrack();
- }
- else
- {
- int listeners = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
- .filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened()).count();
- String msg;
- if(handler.getVotes().contains(event.getAuthor().getId()))
- msg = event.getClient().getWarning()+" You already voted to skip this song `[";
- else
- {
- msg = event.getClient().getSuccess()+" You voted to skip the song `[";
- handler.getVotes().add(event.getAuthor().getId());
- }
- int skippers = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
- .filter(m -> handler.getVotes().contains(m.getUser().getId())).count();
- int required = (int)Math.ceil(listeners * skipRatio);
- msg += skippers + " votes, " + required + "/" + listeners + " needed]`";
- if(skippers>=required)
- {
- msg += "\n" + event.getClient().getSuccess() + " Skipped **" + handler.getPlayer().getPlayingTrack().getInfo().title
- + "** " + (rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)");
- handler.getPlayer().stopTrack();
- }
- event.reply(msg);
- }
+ int listeners = (int) event.getSelfMember().getVoiceState().getChannel().getMembers().stream()
+ .filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened()).count();
+
+ musicService.skipWithVote(event.getGuild(), event.getMember(), listeners, new SimpleOutputAdapter(event));
}
-
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/AdminSlashCommand.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/AdminSlashCommand.java
new file mode 100644
index 000000000..cf9ce46b3
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/AdminSlashCommand.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2;
+
+import com.jagrosh.jdautilities.command.SlashCommand;
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import net.dv8tion.jda.api.Permission;
+
+/**
+ * Base class for Admin-level slash commands.
+ * Requires MANAGE_SERVER permission or being the bot owner.
+ */
+public abstract class AdminSlashCommand extends SlashCommand
+{
+ protected final Bot bot;
+
+ public AdminSlashCommand(Bot bot)
+ {
+ this.bot = bot;
+ this.guildOnly = true;
+ this.category = new Category("Admin");
+ }
+
+ @Override
+ protected void execute(SlashCommandEvent event)
+ {
+ // Check if user is owner or has MANAGE_SERVER permission
+ boolean isOwner = event.getUser().getId().equals(event.getClient().getOwnerId());
+ boolean hasPermission = event.getMember() != null && event.getMember().hasPermission(Permission.MANAGE_SERVER);
+
+ if (!isOwner && !hasPermission)
+ {
+ event.reply(event.getClient().getError() + " You need the Manage Server permission to use this command!")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ doAdminCommand(event);
+ }
+
+ /**
+ * Override this method to implement the admin command logic.
+ */
+ public abstract void doAdminCommand(SlashCommandEvent event);
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/PrefixSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/PrefixSlashCmd.java
new file mode 100644
index 000000000..5dfec7efc
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/PrefixSlashCmd.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to set a server-specific prefix.
+ */
+public class PrefixSlashCmd extends AdminSlashCommand
+{
+ public PrefixSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "prefix";
+ this.help = "sets a server-specific prefix";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "prefix", "the new prefix (leave empty to clear)", false)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+
+ if (event.getOption("prefix") == null)
+ {
+ settings.setPrefix(null);
+ event.reply(event.getClient().getSuccess() + " Prefix cleared.").queue();
+ }
+ else
+ {
+ String prefix = event.getOption("prefix").getAsString();
+ settings.setPrefix(prefix);
+ event.reply(event.getClient().getSuccess() + " Custom prefix set to `" + prefix + "` on *" + event.getGuild().getName() + "*").queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/QueuetypeSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/QueuetypeSlashCmd.java
new file mode 100644
index 000000000..9f39ea697
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/QueuetypeSlashCmd.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.audio.AudioHandler;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.QueueType;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to change the queue type.
+ */
+public class QueuetypeSlashCmd extends AdminSlashCommand
+{
+ public QueuetypeSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "queuetype";
+ this.help = "changes the queue type";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "type", "queue type", false)
+ .addChoice("linear", "LINEAR")
+ .addChoice("fair", "FAIR")
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+
+ if (event.getOption("type") == null)
+ {
+ QueueType currentType = settings.getQueueType();
+ event.reply(currentType.getEmoji() + " Current queue type is: `" + currentType.getUserFriendlyName() + "`.").queue();
+ return;
+ }
+
+ String typeArg = event.getOption("type").getAsString();
+ QueueType value;
+ try
+ {
+ value = QueueType.valueOf(typeArg.toUpperCase());
+ }
+ catch (IllegalArgumentException e)
+ {
+ event.reply(event.getClient().getError() + " Invalid queue type. Valid types are: linear, fair")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ if (settings.getQueueType() != value)
+ {
+ settings.setQueueType(value);
+
+ AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler();
+ if (handler != null)
+ handler.setQueueType(value);
+ }
+
+ event.reply(value.getEmoji() + " Queue type was set to `" + value.getUserFriendlyName() + "`.").queue();
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetdjSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetdjSlashCmd.java
new file mode 100644
index 000000000..a5ebcd11d
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetdjSlashCmd.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.entities.Role;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to set the DJ role.
+ */
+public class SetdjSlashCmd extends AdminSlashCommand
+{
+ public SetdjSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "setdj";
+ this.help = "sets the DJ role for certain music commands";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.ROLE, "role", "the DJ role (leave empty to clear)", false)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+
+ if (event.getOption("role") == null)
+ {
+ settings.setDJRole(null);
+ event.reply(event.getClient().getSuccess() + " DJ role cleared; Only Admins can use the DJ commands.").queue();
+ }
+ else
+ {
+ Role role = event.getOption("role").getAsRole();
+ settings.setDJRole(role);
+ event.reply(event.getClient().getSuccess() + " DJ commands can now be used by users with the **" + role.getName() + "** role.").queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SettcSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SettcSlashCmd.java
new file mode 100644
index 000000000..7aa820473
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SettcSlashCmd.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to set the text channel for music commands.
+ */
+public class SettcSlashCmd extends AdminSlashCommand
+{
+ public SettcSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "settc";
+ this.help = "sets the text channel for music commands";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.CHANNEL, "channel", "the text channel for music commands (leave empty to clear)", false)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+
+ if (event.getOption("channel") == null)
+ {
+ settings.setTextChannel(null);
+ event.reply(event.getClient().getSuccess() + " Music commands can now be used in any channel").queue();
+ }
+ else
+ {
+ GuildChannel channel = event.getOption("channel").getAsChannel();
+ if (!(channel instanceof TextChannel))
+ {
+ event.reply(event.getClient().getError() + " Please select a text channel!")
+ .setEphemeral(true).queue();
+ return;
+ }
+ TextChannel textChannel = (TextChannel) channel;
+ settings.setTextChannel(textChannel);
+ event.reply(event.getClient().getSuccess() + " Music commands can now only be used in " + textChannel.getAsMention()).queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetvcSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetvcSlashCmd.java
new file mode 100644
index 000000000..d38ef5043
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SetvcSlashCmd.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to set the voice channel for playing music.
+ */
+public class SetvcSlashCmd extends AdminSlashCommand
+{
+ public SetvcSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "setvc";
+ this.help = "sets the voice channel for playing music";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.CHANNEL, "channel", "the voice channel for music (leave empty to clear)", false)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+
+ if (event.getOption("channel") == null)
+ {
+ settings.setVoiceChannel(null);
+ event.reply(event.getClient().getSuccess() + " Music can now be played in any channel").queue();
+ }
+ else
+ {
+ GuildChannel channel = event.getOption("channel").getAsChannel();
+ if (!(channel instanceof AudioChannel))
+ {
+ event.reply(event.getClient().getError() + " Please select a voice channel!")
+ .setEphemeral(true).queue();
+ return;
+ }
+ VoiceChannel voiceChannel = (VoiceChannel) channel;
+ settings.setVoiceChannel(voiceChannel);
+ event.reply(event.getClient().getSuccess() + " Music can now only be played in " + voiceChannel.getAsMention()).queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SkipratioSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SkipratioSlashCmd.java
new file mode 100644
index 000000000..a305f5c71
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/admin/SkipratioSlashCmd.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.admin;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.AdminSlashCommand;
+import com.jagrosh.jmusicbot.settings.Settings;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Admin slash command to set the skip vote percentage.
+ */
+public class SkipratioSlashCmd extends AdminSlashCommand
+{
+ public SkipratioSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "setskip";
+ this.help = "sets a server-specific skip percentage";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.INTEGER, "percentage", "skip percentage (0-100)", true)
+ .setMinValue(0)
+ .setMaxValue(100)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doAdminCommand(SlashCommandEvent event)
+ {
+ int value = (int) event.getOption("percentage").getAsLong();
+
+ Settings settings = event.getClient().getSettingsFor(event.getGuild());
+ settings.setSkipRatio(value / 100.0);
+
+ event.reply(event.getClient().getSuccess() + " Skip percentage has been set to `" + value + "%` of listeners on *" + event.getGuild().getName() + "*").queue();
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceremoveSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceremoveSlashCmd.java
new file mode 100644
index 000000000..6ff3549d2
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceremoveSlashCmd.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import com.jagrosh.jmusicbot.utils.FormatUtil;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * DJ slash command to remove all entries by a user from the queue.
+ */
+public class ForceremoveSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+
+ public ForceremoveSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "forceremove";
+ this.help = "removes all entries by a user from the queue";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.USER, "user", "the user whose entries to remove", true)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ if (musicService.isQueueEmpty(event.getGuild()))
+ {
+ event.reply(event.getClient().getError() + " There is nothing in the queue!")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ User target = event.getOption("user").getAsUser();
+ int count = musicService.removeAllTracksByUser(event.getGuild(), target.getIdLong());
+
+ if (count == 0)
+ {
+ event.reply(event.getClient().getWarning() + " **" + target.getName() + "** doesn't have any songs in the queue!")
+ .setEphemeral(true).queue();
+ }
+ else
+ {
+ event.reply(event.getClient().getSuccess() + " Successfully removed `" + count + "` entries from " + FormatUtil.formatUsername(target) + ".")
+ .queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceskipSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceskipSlashCmd.java
new file mode 100644
index 000000000..14893f321
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/ForceskipSlashCmd.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+
+/**
+ * DJ slash command to force skip the current song.
+ */
+public class ForceskipSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+
+ public ForceskipSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "forceskip";
+ this.help = "skips the current song";
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ MusicService.ForceSkipResult result = musicService.forceSkip(event.getGuild());
+ if (result != null)
+ {
+ event.reply(event.getClient().getSuccess() + " Skipped **" + result.trackTitle + "** " + result.requesterInfo).queue();
+ }
+ else
+ {
+ event.reply(event.getClient().getWarning() + " Nothing is currently playing!").setEphemeral(true).queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/MovetrackSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/MovetrackSlashCmd.java
new file mode 100644
index 000000000..2598e8876
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/MovetrackSlashCmd.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Arrays;
+
+/**
+ * DJ slash command to move a track in the queue.
+ */
+public class MovetrackSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+
+ public MovetrackSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "movetrack";
+ this.help = "move a track in the current queue to a different position";
+ this.options = Arrays.asList(
+ new OptionData(OptionType.INTEGER, "from", "current position of the track", true).setMinValue(1),
+ new OptionData(OptionType.INTEGER, "to", "new position for the track", true).setMinValue(1)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ int from = (int) event.getOption("from").getAsLong();
+ int to = (int) event.getOption("to").getAsLong();
+
+ if (from == to)
+ {
+ event.reply(event.getClient().getError() + " Can't move a track to the same position.")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ if (!musicService.isValidQueuePosition(event.getGuild(), from))
+ {
+ event.reply(event.getClient().getError() + " `" + from + "` is not a valid position in the queue!")
+ .setEphemeral(true).queue();
+ return;
+ }
+ if (!musicService.isValidQueuePosition(event.getGuild(), to))
+ {
+ event.reply(event.getClient().getError() + " `" + to + "` is not a valid position in the queue!")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ String trackTitle = musicService.moveTrackPosition(event.getGuild(), from, to);
+ if (trackTitle != null)
+ {
+ event.reply(event.getClient().getSuccess() + " Moved **" + trackTitle + "** from position `" + from + "` to `" + to + "`.")
+ .queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/PlaynextSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/PlaynextSlashCmd.java
new file mode 100644
index 000000000..af48306a1
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/PlaynextSlashCmd.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.commands.v2.SlashOutputAdapters.InteractionHookOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * DJ slash command to play a single song next in the queue.
+ */
+public class PlaynextSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+ private final String loadingEmoji;
+
+ public PlaynextSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.loadingEmoji = bot.getConfig().getLoading();
+ this.name = "playnext";
+ this.help = "plays a single song next";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "query", "song title or URL", true)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = false;
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ String query = event.getOption("query").getAsString();
+
+ event.reply(loadingEmoji + " Loading... `[" + query + "]`").queue(hook ->
+ {
+ musicService.playNext(event.getGuild(), event.getMember(), query, event.getTextChannel(),
+ new InteractionHookOutputAdapter(hook, event.getJDA(), event.getClient().getWarning()));
+ });
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/RepeatSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/RepeatSlashCmd.java
new file mode 100644
index 000000000..b415150de
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/RepeatSlashCmd.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import com.jagrosh.jmusicbot.settings.RepeatMode;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * DJ slash command to set the repeat mode.
+ */
+public class RepeatSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+
+ public RepeatSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "repeat";
+ this.help = "re-adds music to the queue when finished";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "mode", "repeat mode", false)
+ .addChoice("off", "off")
+ .addChoice("all", "all")
+ .addChoice("single", "single")
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ RepeatMode currentMode = musicService.getRepeatMode(event.getGuild());
+ RepeatMode newMode;
+
+ if (event.getOption("mode") == null)
+ {
+ // Toggle between off and all
+ newMode = (currentMode == RepeatMode.OFF) ? RepeatMode.ALL : RepeatMode.OFF;
+ }
+ else
+ {
+ String modeArg = event.getOption("mode").getAsString();
+ switch (modeArg.toLowerCase())
+ {
+ case "off":
+ newMode = RepeatMode.OFF;
+ break;
+ case "all":
+ newMode = RepeatMode.ALL;
+ break;
+ case "single":
+ newMode = RepeatMode.SINGLE;
+ break;
+ default:
+ event.reply(event.getClient().getError() + " Valid options are `off`, `all` or `single`")
+ .setEphemeral(true).queue();
+ return;
+ }
+ }
+
+ musicService.setRepeatMode(event.getGuild(), newMode);
+ event.reply(event.getClient().getSuccess() + " Repeat mode is now `" + newMode.getUserFriendlyName() + "`").queue();
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/SkiptoSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/SkiptoSlashCmd.java
new file mode 100644
index 000000000..af7c0a93d
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/dj/SkiptoSlashCmd.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.dj;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.DJSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * DJ slash command to skip to a specific position in the queue.
+ */
+public class SkiptoSlashCmd extends DJSlashCommand
+{
+ private final MusicService musicService;
+
+ public SkiptoSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "skipto";
+ this.help = "skips to the specified song";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.INTEGER, "position", "queue position to skip to", true)
+ .setMinValue(1)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doDJCommand(SlashCommandEvent event)
+ {
+ int position = (int) event.getOption("position").getAsLong();
+ int queueSize = musicService.getQueueSize(event.getGuild());
+
+ if (position < 1 || position > queueSize)
+ {
+ event.reply(event.getClient().getError() + " Position must be a valid integer between 1 and " + queueSize + "!")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ String trackTitle = musicService.skipToPosition(event.getGuild(), position);
+ if (trackTitle != null)
+ {
+ event.reply(event.getClient().getSuccess() + " Skipped to **" + trackTitle + "**").queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/PlaylistsSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/PlaylistsSlashCmd.java
new file mode 100644
index 000000000..c5186b271
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/PlaylistsSlashCmd.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+
+import java.util.List;
+
+/**
+ * Slash command to show available playlists.
+ */
+public class PlaylistsSlashCmd extends MusicSlashCommand
+{
+ public PlaylistsSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.name = "playlists";
+ this.help = "shows the available playlists";
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = false;
+ this.bePlaying = false;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ if (!bot.getPlaylistLoader().folderExists())
+ bot.getPlaylistLoader().createFolder();
+
+ if (!bot.getPlaylistLoader().folderExists())
+ {
+ event.reply(event.getClient().getWarning() + " Playlists folder does not exist and could not be created!")
+ .setEphemeral(true).queue();
+ return;
+ }
+
+ List list = bot.getPlaylistLoader().getPlaylistNames();
+ if (list == null)
+ {
+ event.reply(event.getClient().getError() + " Failed to load available playlists!")
+ .setEphemeral(true).queue();
+ }
+ else if (list.isEmpty())
+ {
+ event.reply(event.getClient().getWarning() + " There are no playlists in the Playlists folder!")
+ .setEphemeral(true).queue();
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder(event.getClient().getSuccess() + " Available playlists:\n");
+ list.forEach(str -> builder.append("`").append(str).append("` "));
+ builder.append("\nUse `/play playlist:` to play a playlist");
+ event.reply(builder.toString()).queue();
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/QueueSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/QueueSlashCmd.java
new file mode 100644
index 000000000..48148c0a4
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/QueueSlashCmd.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import com.jagrosh.jmusicbot.utils.TimeUtil;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+
+import java.util.Collections;
+
+/**
+ * Slash command to show the current queue.
+ */
+public class QueueSlashCmd extends MusicSlashCommand
+{
+ private static final int TRACKS_PER_PAGE = 10;
+ private final MusicService musicService;
+
+ public QueueSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "queue";
+ this.help = "shows the current queue";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.INTEGER, "page", "page number to display", false)
+ .setMinValue(1)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ int page = event.getOption("page") != null ? (int) event.getOption("page").getAsLong() : 1;
+
+ MusicService.QueueInfo queueInfo = musicService.getQueueInfo(event.getGuild(), event.getJDA());
+ if (queueInfo == null || queueInfo.isEmpty())
+ {
+ MusicService.NowPlayingInfo npInfo = musicService.getNowPlayingInfo(event.getGuild(), event.getJDA());
+ if (npInfo != null)
+ {
+ MessageCreateData embed = npInfo.isPlaying ? npInfo.nowPlayingMessage : npInfo.noMusicMessage;
+ if (embed != null)
+ {
+ MessageCreateData built = new MessageCreateBuilder()
+ .setContent(event.getClient().getWarning() + " There is no music in the queue!")
+ .setEmbeds(embed.getEmbeds().get(0)).build();
+ event.reply(built).queue(hook ->
+ {
+ if (npInfo.isPlaying)
+ hook.retrieveOriginal().queue(msg -> bot.getNowplayingHandler().setLastNPMessage(msg));
+ });
+ return;
+ }
+ }
+ event.reply(event.getClient().getWarning() + " There is no music in the queue!").setEphemeral(true).queue();
+ return;
+ }
+
+ // Build paginated response
+ int totalPages = (int) Math.ceil((double) queueInfo.tracks.length / TRACKS_PER_PAGE);
+ if (page > totalPages)
+ {
+ page = totalPages;
+ }
+
+ int startIndex = (page - 1) * TRACKS_PER_PAGE;
+ int endIndex = Math.min(startIndex + TRACKS_PER_PAGE, queueInfo.tracks.length);
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = startIndex; i < endIndex; i++)
+ {
+ sb.append("`").append(i + 1).append(".` ").append(queueInfo.tracks[i]).append("\n");
+ }
+
+ String title = musicService.formatQueueTitle(queueInfo, event.getClient().getSuccess());
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setTitle("Queue - Page " + page + "/" + totalPages)
+ .setDescription(sb.toString())
+ .setFooter("Total: " + queueInfo.tracks.length + " tracks | Duration: " + TimeUtil.formatTime(queueInfo.totalDuration))
+ .setColor(event.getMember().getColor());
+
+ event.reply(title).addEmbeds(embed.build()).queue();
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/RemoveSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/RemoveSlashCmd.java
new file mode 100644
index 000000000..ca1c16b0f
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/RemoveSlashCmd.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.commands.v2.SlashOutputAdapters.SlashEventOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Slash command to remove a track from the queue.
+ */
+public class RemoveSlashCmd extends MusicSlashCommand
+{
+ private final MusicService musicService;
+
+ public RemoveSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "remove";
+ this.help = "removes a song from the queue";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "position", "queue position to remove, or 'all' to remove all your songs", true)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ String positionArg = event.getOption("position").getAsString();
+ SlashEventOutputAdapter output = new SlashEventOutputAdapter(event);
+
+ if (musicService.isQueueEmpty(event.getGuild()))
+ {
+ output.replyError("There is nothing in the queue!");
+ return;
+ }
+
+ if (positionArg.equalsIgnoreCase("all"))
+ {
+ musicService.removeAllTracks(event.getGuild(), event.getMember(), output);
+ return;
+ }
+
+ int pos;
+ try
+ {
+ pos = Integer.parseInt(positionArg);
+ }
+ catch (NumberFormatException e)
+ {
+ output.replyError("Please provide a valid position number or 'all'.");
+ return;
+ }
+
+ musicService.removeTrack(event.getGuild(), event.getMember(), pos, output);
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
new file mode 100644
index 000000000..9503e2b8b
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+import com.jagrosh.jmusicbot.service.SearchService;
+import com.jagrosh.jmusicbot.utils.FormatUtil;
+import com.jagrosh.jmusicbot.utils.TimeUtil;
+import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
+import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.components.actionrow.ActionRow;
+import net.dv8tion.jda.api.components.selections.StringSelectMenu;
+import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Slash command to search for tracks and select from results.
+ */
+public class SearchSlashCmd extends MusicSlashCommand
+{
+ protected String searchPrefix = "ytsearch:";
+ protected String searchPlatform = "YouTube";
+ private final MusicService musicService;
+ private final String searchingEmoji;
+
+ public SearchSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.searchingEmoji = bot.getConfig().getSearching();
+ this.name = "search";
+ this.help = "searches YouTube for a provided query";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "query", "the search query", true)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = false;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ String query = event.getOption("query").getAsString();
+
+ event.reply(searchingEmoji + " Searching " + searchPlatform + " for `" + query + "`...").queue(hook ->
+ {
+ bot.getPlayerManager().loadItemOrdered(event.getGuild(), searchPrefix + query,
+ new SearchService.SearchCallback()
+ {
+ @Override
+ public void onTrackLoaded(AudioTrack track, int queuePosition, String formattedMessage)
+ {
+ hook.editOriginal(event.getClient().getSuccess() + " " + formattedMessage).queue();
+ }
+
+ @Override
+ public void onSearchResults(AudioPlaylist playlist, String[] formattedChoices)
+ {
+ if (playlist.getTracks().isEmpty())
+ {
+ hook.editOriginal(event.getClient().getWarning() + " No results found for `" + query + "`.").queue();
+ return;
+ }
+
+ // Build select menu for search results
+ StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("search_select_" + event.getUser().getId())
+ .setPlaceholder("Select a track to play")
+ .setMinValues(1)
+ .setMaxValues(1);
+
+ int limit = Math.min(5, playlist.getTracks().size());
+ StringBuilder description = new StringBuilder();
+ for (int i = 0; i < limit; i++)
+ {
+ AudioTrack track = playlist.getTracks().get(i);
+ String title = track.getInfo().title;
+ if (title.length() > 80)
+ {
+ title = title.substring(0, 77) + "...";
+ }
+ menuBuilder.addOption(
+ title,
+ track.getInfo().uri,
+ "Duration: " + TimeUtil.formatTime(track.getDuration())
+ );
+ description.append("`").append(i + 1).append(".` ")
+ .append("[**").append(FormatUtil.filter(track.getInfo().title)).append("**](")
+ .append(track.getInfo().uri).append(") `[")
+ .append(TimeUtil.formatTime(track.getDuration())).append("]`\n");
+ }
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setTitle("Search Results for: " + query)
+ .setDescription(description.toString())
+ .setColor(event.getMember().getColor())
+ .setFooter("Select a track from the menu below");
+
+ hook.editOriginalEmbeds(embed.build())
+ .setContent("")
+ .setComponents(ActionRow.of(menuBuilder.build()))
+ .queue(msg ->
+ {
+ // Wait for selection
+ bot.getWaiter().waitForEvent(
+ StringSelectInteractionEvent.class,
+ e -> e.getMessageId().equals(msg.getId()) &&
+ e.getUser().getIdLong() == event.getUser().getIdLong(),
+ e ->
+ {
+ String selectedUri = e.getValues().get(0);
+ e.deferEdit().queue();
+
+ // Find the selected track
+ AudioTrack selectedTrack = null;
+ for (AudioTrack track : playlist.getTracks())
+ {
+ if (track.getInfo().uri.equals(selectedUri))
+ {
+ selectedTrack = track;
+ break;
+ }
+ }
+
+ if (selectedTrack != null)
+ {
+ MusicService.TrackAddResult result = musicService.addTrackToQueue(
+ event.getGuild(),
+ event.getMember(),
+ selectedTrack,
+ query,
+ event.getTextChannel()
+ );
+
+ if (result != null)
+ {
+ hook.editOriginal(event.getClient().getSuccess() + " " + result.formattedMessage)
+ .setEmbeds()
+ .setComponents()
+ .queue();
+ }
+ else
+ {
+ hook.editOriginal(event.getClient().getWarning() + " " + musicService.formatTooLongError(selectedTrack))
+ .setEmbeds()
+ .setComponents()
+ .queue();
+ }
+ }
+ },
+ 1, TimeUnit.MINUTES,
+ () -> hook.editOriginal("Search timed out.")
+ .setEmbeds()
+ .setComponents()
+ .queue()
+ );
+ });
+ }
+
+ @Override
+ public void onNoMatches(String searchQuery)
+ {
+ hook.editOriginal(event.getClient().getWarning() + " No results found for `" + searchQuery + "`.").queue();
+ }
+
+ @Override
+ public void onLoadFailed(String errorMessage)
+ {
+ hook.editOriginal(event.getClient().getError() + " " + errorMessage).queue();
+ }
+
+ @Override
+ public void onError(String message)
+ {
+ hook.editOriginal(event.getClient().getError() + " " + message).queue();
+ }
+ });
+ });
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SeekSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SeekSlashCmd.java
new file mode 100644
index 000000000..07bf0b7b0
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SeekSlashCmd.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.commands.v2.SlashOutputAdapters.SlashEventOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+import java.util.Collections;
+
+/**
+ * Slash command to seek to a position in the current track.
+ */
+public class SeekSlashCmd extends MusicSlashCommand
+{
+ private final MusicService musicService;
+
+ public SeekSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "seek";
+ this.help = "seeks the current song";
+ this.options = Collections.singletonList(
+ new OptionData(OptionType.STRING, "time", "[+ | -] or <0h0m0s>", true)
+ );
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ String timeString = event.getOption("time").getAsString();
+ musicService.seek(event.getGuild(), event.getMember(), timeString, new SlashEventOutputAdapter(event));
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/ShuffleSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/ShuffleSlashCmd.java
new file mode 100644
index 000000000..6220e488b
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/ShuffleSlashCmd.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.service.MusicService;
+
+/**
+ * Slash command to shuffle the user's tracks in the queue.
+ */
+public class ShuffleSlashCmd extends MusicSlashCommand
+{
+ private final MusicService musicService;
+
+ public ShuffleSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "shuffle";
+ this.help = "shuffles songs you have added";
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ int shuffled = musicService.shuffleUserTracks(event.getGuild(), event.getUser().getIdLong());
+ switch (shuffled)
+ {
+ case 0:
+ event.reply(event.getClient().getError() + " You don't have any music in the queue to shuffle!")
+ .setEphemeral(true).queue();
+ break;
+ case 1:
+ event.reply(event.getClient().getWarning() + " You only have one song in the queue!")
+ .setEphemeral(true).queue();
+ break;
+ default:
+ event.reply(event.getClient().getSuccess() + " You successfully shuffled your " + shuffled + " entries.")
+ .queue();
+ break;
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SkipSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SkipSlashCmd.java
new file mode 100644
index 000000000..7a7b1165b
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SkipSlashCmd.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.commands.v2.music;
+
+import com.jagrosh.jdautilities.command.SlashCommandEvent;
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.commands.v2.MusicSlashCommand;
+import com.jagrosh.jmusicbot.commands.v2.SlashOutputAdapters.SlashEventOutputAdapter;
+import com.jagrosh.jmusicbot.service.MusicService;
+
+/**
+ * Slash command to vote to skip the current song.
+ */
+public class SkipSlashCmd extends MusicSlashCommand
+{
+ private final MusicService musicService;
+
+ public SkipSlashCmd(Bot bot)
+ {
+ super(bot);
+ this.musicService = bot.getMusicService();
+ this.name = "skip";
+ this.help = "votes to skip the current song";
+ this.aliases = bot.getConfig().getAliases(this.name);
+ this.beListening = true;
+ this.bePlaying = true;
+ }
+
+ @Override
+ public void doCommand(SlashCommandEvent event)
+ {
+ int listeners = (int) event.getMember().getVoiceState().getChannel().getMembers().stream()
+ .filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened()).count();
+
+ musicService.skipWithVote(event.getGuild(), event.getMember(), listeners, new SlashEventOutputAdapter(event));
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/service/AudioLoadResultHandlers.java b/src/main/java/com/jagrosh/jmusicbot/service/AudioLoadResultHandlers.java
new file mode 100644
index 000000000..7e0b3d6a9
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/service/AudioLoadResultHandlers.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2026 Arif Banai (arif-banai)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jagrosh.jmusicbot.service;
+
+import com.jagrosh.jmusicbot.Bot;
+import com.jagrosh.jmusicbot.audio.AudioHandler;
+import com.jagrosh.jmusicbot.audio.QueuedTrack;
+import com.jagrosh.jmusicbot.audio.RequestMetadata;
+import com.jagrosh.jmusicbot.utils.FormatUtil;
+import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
+import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
+import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
+import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
+import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.components.actionrow.ActionRow;
+import net.dv8tion.jda.api.components.buttons.Button;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.emoji.Emoji;
+import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
+import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Audio load result handlers for processing track/playlist loading results.
+ * These handlers are used by MusicService for play and playNext operations.
+ */
+public final class AudioLoadResultHandlers
+{
+ private static final Logger LOG = LoggerFactory.getLogger(AudioLoadResultHandlers.class);
+
+ private AudioLoadResultHandlers()
+ {
+ // Utility class - prevent instantiation
+ }
+
+ /**
+ * Base class for audio load result handlers with shared fields and common logic.
+ */
+ public static abstract class BaseResultHandler implements AudioLoadResultHandler
+ {
+ protected final MusicService musicService;
+ protected final Bot bot;
+ protected final MusicService.OutputAdapter output;
+ protected final Guild guild;
+ protected final Member member;
+ protected final String args;
+ protected final boolean ytsearch;
+ protected final TextChannel channel;
+
+ protected BaseResultHandler(MusicService musicService, Bot bot, MusicService.OutputAdapter output,
+ Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
+ {
+ this.musicService = musicService;
+ this.bot = bot;
+ this.output = output;
+ this.guild = guild;
+ this.member = member;
+ this.args = args;
+ this.ytsearch = ytsearch;
+ this.channel = channel;
+ }
+
+ /**
+ * Creates a fallback handler for YouTube search when no direct match is found.
+ */
+ protected abstract BaseResultHandler createFallbackHandler();
+
+ /**
+ * Returns a descriptive name for logging (e.g., "Track" or "PlayNext").
+ */
+ protected abstract String getHandlerName();
+
+ @Override
+ public void noMatches()
+ {
+ if (ytsearch)
+ {
+ LOG.debug("{} no matches found: guild={}, query=\"{}\"", getHandlerName(), guild.getId(), args);
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " No results found for `" + args + "`."));
+ }
+ else
+ {
+ LOG.debug("{} falling back to YouTube search: guild={}, query=\"{}\"",
+ getHandlerName(), guild.getId(), args);
+ bot.getPlayerManager().loadItemOrdered(guild, "ytsearch:" + args, createFallbackHandler());
+ }
+ }
+
+ @Override
+ public void loadFailed(FriendlyException throwable)
+ {
+ if (throwable.severity == Severity.COMMON)
+ {
+ LOG.warn("{} load failed (common): guild={}, query=\"{}\", error={}",
+ getHandlerName(), guild.getId(), args, throwable.getMessage());
+ output.editMessage(bot.getConfig().getError() + " Error loading: " + throwable.getMessage());
+ }
+ else
+ {
+ LOG.error("{} load failed (severe): guild={}, query=\"{}\"",
+ getHandlerName(), guild.getId(), args, throwable);
+ output.editMessage(bot.getConfig().getError() + " Error loading track.");
+ }
+ }
+ }
+
+ /**
+ * Result handler for standard play command that adds tracks to the end of the queue.
+ */
+ public static class PlayResultHandler extends BaseResultHandler
+ {
+ private static final String LOAD = "\uD83D\uDCE5"; // 📥
+ private static final String CANCEL = "\uD83D\uDEAB"; // 🚫
+
+ public PlayResultHandler(MusicService musicService, Bot bot, MusicService.OutputAdapter output,
+ Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
+ {
+ super(musicService, bot, output, guild, member, args, ytsearch, channel);
+ }
+
+ @Override
+ protected BaseResultHandler createFallbackHandler()
+ {
+ return new PlayResultHandler(musicService, bot, output, guild, member, args, true, channel);
+ }
+
+ @Override
+ protected String getHandlerName()
+ {
+ return "Track";
+ }
+
+ private void loadSingle(AudioTrack track, AudioPlaylist playlist)
+ {
+ MusicService.TrackAddResult result = musicService.addTrackToQueue(guild, member, track, args, channel);
+ if (result == null)
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " " + musicService.formatTooLongError(track)));
+ return;
+ }
+
+ String addMsg = FormatUtil.filter(bot.getConfig().getSuccess() + " " + result.formattedMessage);
+ if (playlist == null || !guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_ADD_REACTION))
+ {
+ output.editMessage(addMsg);
+ }
+ else
+ {
+ promptForPlaylistLoad(track, playlist, addMsg);
+ }
+ }
+
+ private void promptForPlaylistLoad(AudioTrack track, AudioPlaylist playlist, String addMsg)
+ {
+ String promptMsg = addMsg + "\n" + bot.getConfig().getWarning() + " This track has a playlist of **"
+ + playlist.getTracks().size() + "** tracks attached. Select " + LOAD + " to load playlist.";
+
+ MessageEditBuilder editBuilder = new MessageEditBuilder()
+ .setContent(promptMsg)
+ .setComponents(ActionRow.of(
+ Button.success("load_playlist", Emoji.fromUnicode(LOAD)).withLabel("Load Playlist"),
+ Button.danger("cancel_playlist", Emoji.fromUnicode(CANCEL)).withLabel("Cancel")
+ ));
+
+ output.editMessage(addMsg, m -> {
+ m.editMessage(editBuilder.build()).queue(msg -> {
+ bot.getWaiter().waitForEvent(ButtonInteractionEvent.class,
+ event -> event.getMessageId().equals(msg.getId()) &&
+ (event.getComponentId().equals("load_playlist") || event.getComponentId().equals("cancel_playlist")) &&
+ event.getUser().getIdLong() == member.getIdLong(),
+ event -> {
+ if (event.getComponentId().equals("load_playlist"))
+ {
+ int loaded = loadPlaylist(playlist, track);
+ event.editMessage(addMsg + "\n" + bot.getConfig().getSuccess() + " Loaded **" + loaded + "** additional tracks!").setComponents().queue();
+ }
+ else
+ {
+ event.editMessage(addMsg).setComponents().queue();
+ }
+ },
+ 30, TimeUnit.SECONDS,
+ () -> msg.editMessage(addMsg).setComponents().queue());
+ });
+ });
+ }
+
+ private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
+ {
+ int[] count = {0};
+ AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ playlist.getTracks().forEach((track) -> {
+ if (!musicService.isTooLong(track) && !track.equals(exclude))
+ {
+ handler.setLastReason(member.getUser().getName() + " added a playlist.");
+ handler.addTrack(new QueuedTrack(track,
+ new RequestMetadata(member.getUser(),
+ new RequestMetadata.RequestInfo(args, track.getInfo().uri),
+ channel.getIdLong())));
+ count[0]++;
+ }
+ });
+ return count[0];
+ }
+
+ @Override
+ public void trackLoaded(AudioTrack track)
+ {
+ LOG.debug("Track loaded: guild={}, track=\"{}\"", guild.getId(), track.getInfo().title);
+ loadSingle(track, null);
+ }
+
+ @Override
+ public void playlistLoaded(AudioPlaylist playlist)
+ {
+ LOG.debug("Playlist loaded: guild={}, name=\"{}\", tracks={}",
+ guild.getId(), playlist.getName(), playlist.getTracks().size());
+
+ if (playlist.getTracks().size() == 1 || playlist.isSearchResult())
+ {
+ AudioTrack single = playlist.getSelectedTrack() == null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
+ loadSingle(single, null);
+ }
+ else if (playlist.getSelectedTrack() != null)
+ {
+ AudioTrack single = playlist.getSelectedTrack();
+ loadSingle(single, playlist);
+ }
+ else
+ {
+ int count = loadPlaylist(playlist, null);
+ handlePlaylistLoadResult(playlist, count);
+ }
+ }
+
+ private void handlePlaylistLoadResult(AudioPlaylist playlist, int count)
+ {
+ if (playlist.getTracks().size() == 0)
+ {
+ LOG.warn("Playlist empty or could not be loaded: guild={}, name=\"{}\"",
+ guild.getId(), playlist.getName());
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " The playlist "
+ + (playlist.getName() == null ? "" : "(**" + playlist.getName() + "**) ")
+ + " could not be loaded or contained 0 entries"));
+ }
+ else if (count == 0)
+ {
+ LOG.warn("All playlist tracks too long: guild={}, name=\"{}\"",
+ guild.getId(), playlist.getName());
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " All entries in this playlist "
+ + (playlist.getName() == null ? "" : "(**" + playlist.getName() + "**) ")
+ + "were longer than the allowed maximum (`" + bot.getConfig().getMaxTime() + "`)"));
+ }
+ else
+ {
+ LOG.info("Playlist added to queue: guild={}, name=\"{}\", tracksAdded={}/{}",
+ guild.getId(), playlist.getName(), count, playlist.getTracks().size());
+ output.editMessage(FormatUtil.filter(bot.getConfig().getSuccess() + " Found "
+ + (playlist.getName() == null ? "a playlist" : "playlist **" + playlist.getName() + "**") + " with `"
+ + playlist.getTracks().size() + "` entries; added to the queue!"
+ + (count < playlist.getTracks().size() ? "\n" + bot.getConfig().getWarning()
+ + " Tracks longer than the allowed maximum (`" + bot.getConfig().getMaxTime() + "`) have been omitted." : "")));
+ }
+ }
+ }
+
+ /**
+ * Result handler for playNext command that adds tracks to the front of the queue.
+ */
+ public static class PlayNextResultHandler extends BaseResultHandler
+ {
+ public PlayNextResultHandler(MusicService musicService, Bot bot, MusicService.OutputAdapter output,
+ Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
+ {
+ super(musicService, bot, output, guild, member, args, ytsearch, channel);
+ }
+
+ @Override
+ protected BaseResultHandler createFallbackHandler()
+ {
+ return new PlayNextResultHandler(musicService, bot, output, guild, member, args, true, channel);
+ }
+
+ @Override
+ protected String getHandlerName()
+ {
+ return "PlayNext";
+ }
+
+ private void loadSingle(AudioTrack track)
+ {
+ LOG.debug("PlayNext loading track: guild={}, track=\"{}\"", guild.getId(), track.getInfo().title);
+
+ MusicService.TrackAddResult result = musicService.addTrackToFront(guild, member, track, args, channel);
+ if (result == null)
+ {
+ output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " " + musicService.formatTooLongError(track)));
+ return;
+ }
+
+ output.editMessage(FormatUtil.filter(bot.getConfig().getSuccess() + " " + result.formattedMessage));
+ }
+
+ @Override
+ public void trackLoaded(AudioTrack track)
+ {
+ LOG.debug("PlayNext track loaded: guild={}, track=\"{}\"", guild.getId(), track.getInfo().title);
+ loadSingle(track);
+ }
+
+ @Override
+ public void playlistLoaded(AudioPlaylist playlist)
+ {
+ LOG.debug("PlayNext playlist loaded (selecting single): guild={}, name=\"{}\", tracks={}",
+ guild.getId(), playlist.getName(), playlist.getTracks().size());
+
+ AudioTrack single;
+ if (playlist.getTracks().size() == 1 || playlist.isSearchResult())
+ {
+ single = playlist.getSelectedTrack() == null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
+ }
+ else if (playlist.getSelectedTrack() != null)
+ {
+ single = playlist.getSelectedTrack();
+ }
+ else
+ {
+ single = playlist.getTracks().get(0);
+ }
+ loadSingle(single);
+ }
+ }
+}
diff --git a/src/main/java/com/jagrosh/jmusicbot/service/MusicService.java b/src/main/java/com/jagrosh/jmusicbot/service/MusicService.java
index 5c9fb4977..d4a475565 100644
--- a/src/main/java/com/jagrosh/jmusicbot/service/MusicService.java
+++ b/src/main/java/com/jagrosh/jmusicbot/service/MusicService.java
@@ -26,26 +26,17 @@
import com.jagrosh.jmusicbot.settings.Settings;
import com.jagrosh.jmusicbot.utils.FormatUtil;
import com.jagrosh.jmusicbot.utils.TimeUtil;
-import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
-import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
-import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity;
-import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.Permission;
-import net.dv8tion.jda.api.components.actionrow.ActionRow;
-import net.dv8tion.jda.api.components.buttons.Button;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
-import net.dv8tion.jda.api.entities.emoji.Emoji;
-import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
-import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.List;
-import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -54,11 +45,66 @@
*/
public class MusicService
{
+ private static final Logger LOG = LoggerFactory.getLogger(MusicService.class);
+
private final Bot bot;
public MusicService(Bot bot)
{
this.bot = bot;
+ LOG.info("MusicService initialized");
+ }
+
+ // ========== Internal Helpers ==========
+
+ /**
+ * Gets the AudioHandler for a guild.
+ *
+ * @param guild The guild
+ * @return The AudioHandler, or null if none exists
+ */
+ private AudioHandler getHandler(Guild guild)
+ {
+ return (AudioHandler) guild.getAudioManager().getSendingHandler();
+ }
+
+ /**
+ * Gets the Settings for a guild.
+ *
+ * @param guild The guild
+ * @return The guild's Settings
+ */
+ private Settings getSettings(Guild guild)
+ {
+ return bot.getSettingsManager().getSettings(guild);
+ }
+
+ /**
+ * Checks if the member has DJ permission and sends an error if not.
+ *
+ * @param guild The guild
+ * @param member The member to check
+ * @param output The output adapter for error messages
+ * @param action Description of the action being attempted (for error message)
+ * @return true if the member has DJ permission, false otherwise
+ */
+ private boolean requireDJPermission(Guild guild, Member member, OutputAdapter output, String action)
+ {
+ if (!DJCommand.checkDJPermission(bot, guild, member))
+ {
+ output.replyError("You need to be a DJ to " + action + "!");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Functional interface for track adding strategies.
+ */
+ @FunctionalInterface
+ private interface TrackAdder
+ {
+ int add(AudioHandler handler, QueuedTrack track);
}
// ========== Shared Track Utilities ==========
@@ -102,70 +148,156 @@ public String formatTrackAddedMessage(String title, long duration, int position)
}
/**
- * Adds a track to the queue and returns the result.
+ * Internal helper that handles common track-add logic.
*
- * @param guild The guild
- * @param member The member adding the track
- * @param track The track to add
- * @param queryArgs The original query/args used to find this track
- * @param channel The text channel for request metadata
+ * @param guild The guild
+ * @param member The member adding the track
+ * @param track The track to add
+ * @param queryArgs The original query/args used to find this track
+ * @param channel The text channel for request metadata
+ * @param adder The strategy for adding the track to the queue
+ * @param reason The reason to log (e.g., "added to the queue")
+ * @param logLocation Description for logging (e.g., "queue" or "front of queue")
* @return TrackAddResult containing position and formatted message, or null if track is too long
*/
- public TrackAddResult addTrackToQueue(Guild guild, Member member, AudioTrack track,
- String queryArgs, TextChannel channel)
+ private TrackAddResult addTrackInternal(Guild guild, Member member, AudioTrack track,
+ String queryArgs, TextChannel channel,
+ TrackAdder adder, String reason, String logLocation)
{
+ LOG.debug("Adding track to {}: guild={}, user={}, track={}",
+ logLocation, guild.getId(), member.getUser().getName(), track.getInfo().title);
+
if (isTooLong(track))
{
+ LOG.warn("Track rejected (too long): {} - duration: {} > max: {}",
+ track.getInfo().title, TimeUtil.formatTime(track.getDuration()), bot.getConfig().getMaxTime());
return null;
}
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- handler.setLastReason(member.getUser().getName() + " added to the queue.");
- int position = handler.addTrack(new QueuedTrack(track,
+ AudioHandler handler = getHandler(guild);
+ handler.setLastReason(member.getUser().getName() + " " + reason);
+ QueuedTrack queuedTrack = new QueuedTrack(track,
new RequestMetadata(member.getUser(),
new RequestMetadata.RequestInfo(queryArgs, track.getInfo().uri),
- channel.getIdLong()))) + 1;
+ channel.getIdLong()));
+ int position = adder.add(handler, queuedTrack) + 1;
String title = FormatUtil.getTrackTitle(track);
String message = formatTrackAddedMessage(title, track.getDuration(), position);
+
+ LOG.info("Track added to {}: guild={}, user={}, track=\"{}\", position={}",
+ logLocation, guild.getId(), member.getUser().getName(), title, position);
+
return new TrackAddResult(position, message, title);
}
+ /**
+ * Adds a track to the queue and returns the result.
+ *
+ * @param guild The guild
+ * @param member The member adding the track
+ * @param track The track to add
+ * @param queryArgs The original query/args used to find this track
+ * @param channel The text channel for request metadata
+ * @return TrackAddResult containing position and formatted message, or null if track is too long
+ */
+ public TrackAddResult addTrackToQueue(Guild guild, Member member, AudioTrack track,
+ String queryArgs, TextChannel channel)
+ {
+ return addTrackInternal(guild, member, track, queryArgs, channel,
+ AudioHandler::addTrack, "added to the queue.", "queue");
+ }
+
+ /**
+ * Adds a track to the front of the queue and returns the result.
+ *
+ * @param guild The guild
+ * @param member The member adding the track
+ * @param track The track to add
+ * @param queryArgs The original query/args used to find this track
+ * @param channel The text channel for request metadata
+ * @return TrackAddResult containing position and formatted message, or null if track is too long
+ */
+ public TrackAddResult addTrackToFront(Guild guild, Member member, AudioTrack track,
+ String queryArgs, TextChannel channel)
+ {
+ return addTrackInternal(guild, member, track, queryArgs, channel,
+ AudioHandler::addTrackToFront, "added to the front of the queue.", "front of queue");
+ }
+
// ========== Player Operations ==========
+ public void playNext(Guild guild, Member member, String args, TextChannel channel, OutputAdapter output)
+ {
+ LOG.debug("PlayNext requested: guild={}, user={}, query={}",
+ guild.getId(), member.getUser().getName(), args);
+
+ if (args == null || args.isEmpty())
+ {
+ LOG.debug("PlayNext rejected: empty query");
+ output.replyWarning("Please include a song title or URL!");
+ return;
+ }
+
+ if (args.startsWith("<") && args.endsWith(">"))
+ args = args.substring(1, args.length() - 1);
+
+ LOG.info("Loading track for playNext: guild={}, user={}, query=\"{}\"",
+ guild.getId(), member.getUser().getName(), args);
+
+ bot.getPlayerManager().loadItemOrdered(guild, args,
+ new AudioLoadResultHandlers.PlayNextResultHandler(this, bot, output, guild, member, args, false, channel));
+ }
+
public void play(Guild guild, Member member, String args, TextChannel channel, OutputAdapter output)
{
+ LOG.debug("Play requested: guild={}, user={}, args={}",
+ guild.getId(), member.getUser().getName(), args);
+
if (args != null && args.startsWith("\"") && args.endsWith("\""))
args = args.substring(1, args.length() - 1);
if (args == null || args.isEmpty())
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
if (handler.getPlayer().getPlayingTrack() != null && handler.getPlayer().isPaused())
{
if (DJCommand.checkDJPermission(bot, guild, member))
{
handler.getPlayer().setPaused(false);
+ LOG.info("Playback resumed: guild={}, user={}, track=\"{}\"",
+ guild.getId(), member.getUser().getName(), handler.getPlayer().getPlayingTrack().getInfo().title);
output.replySuccess("Resumed **" + handler.getPlayer().getPlayingTrack().getInfo().title + "**.");
}
else
+ {
+ LOG.debug("Resume rejected: user lacks DJ permission");
output.replyError("Only DJs can unpause the player!");
+ }
return;
}
output.onShowHelp();
return;
}
- bot.getPlayerManager().loadItemOrdered(guild, args, new ResultHandler(output, guild, member, args, false, channel));
+ LOG.info("Loading track: guild={}, user={}, query=\"{}\"",
+ guild.getId(), member.getUser().getName(), args);
+
+ bot.getPlayerManager().loadItemOrdered(guild, args,
+ new AudioLoadResultHandlers.PlayResultHandler(this, bot, output, guild, member, args, false, channel));
}
public void previous(Guild guild, Member member, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ LOG.debug("Previous track requested: guild={}, user={}",
+ guild.getId(), member.getUser().getName());
+
+ AudioHandler handler = getHandler(guild);
boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
if (!isDJ && handler.getRequestMetadata().getOwner() != member.getIdLong())
{
+ LOG.debug("Previous rejected: user lacks permission");
output.replyError("You need to be a DJ or the requester to go back!");
return;
}
@@ -174,12 +306,14 @@ public void previous(Guild guild, Member member, OutputAdapter output)
if (playing != null && playing.getPosition() > 5000)
{
playing.setPosition(0);
+ LOG.info("Track restarted: guild={}, track=\"{}\"", guild.getId(), playing.getInfo().title);
output.replySuccess("Restarted **" + playing.getInfo().title + "**");
return;
}
if (handler.getQueue().getHistory().isEmpty())
{
+ LOG.debug("Previous rejected: no history available");
output.replyError("There are no previous tracks!");
return;
}
@@ -193,39 +327,53 @@ public void previous(Guild guild, Member member, OutputAdapter output)
if (previous != null)
{
handler.getPlayer().playTrack(previous.getTrack());
+ LOG.info("Went to previous track: guild={}, track=\"{}\"",
+ guild.getId(), previous.getTrack().getInfo().title);
output.replySuccess("Went back to **" + previous.getTrack().getInfo().title + "**");
}
else
{
+ LOG.debug("Previous failed: no previous tracks in history");
output.replyError("There are no previous tracks!");
}
}
public void shuffle(Guild guild, Member member, int startIndex, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
-
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
+ if (!requireDJPermission(guild, member, output, "use this button"))
return;
- }
+
+ AudioHandler handler = getHandler(guild);
int s = handler.getQueue().shuffle(startIndex);
output.replySuccess("Shuffled " + s + " tracks!");
}
- public void cycleRepeatMode(Guild guild, Member member, OutputAdapter output)
+ /**
+ * Shuffles only the tracks added by a specific user.
+ *
+ * @param guild The guild
+ * @param userId The user ID whose tracks to shuffle
+ * @return The number of tracks shuffled
+ */
+ public int shuffleUserTracks(Guild guild, long userId)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
+ LOG.debug("Shuffling user tracks: guild={}, userId={}", guild.getId(), userId);
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
+ AudioHandler handler = getHandler(guild);
+ int shuffled = handler.getQueue().shuffle(userId);
+
+ LOG.info("User tracks shuffled: guild={}, userId={}, count={}", guild.getId(), userId, shuffled);
+
+ return shuffled;
+ }
+
+ public void cycleRepeatMode(Guild guild, Member member, OutputAdapter output)
+ {
+ if (!requireDJPermission(guild, member, output, "use this button"))
return;
- }
- RepeatMode mode = bot.getSettingsManager().getSettings(guild).getRepeatMode();
+
+ AudioHandler handler = getHandler(guild);
+ RepeatMode mode = getSettings(guild).getRepeatMode();
RepeatMode nextMode;
switch (mode) {
case OFF:
@@ -239,59 +387,173 @@ public void cycleRepeatMode(Guild guild, Member member, OutputAdapter output)
nextMode = RepeatMode.OFF;
break;
}
- bot.getSettingsManager().getSettings(guild).setRepeatMode(nextMode);
+ getSettings(guild).setRepeatMode(nextMode);
output.editNowPlaying(handler);
}
- public void adjustVolume(Guild guild, Member member, int change, OutputAdapter output)
+ /**
+ * Gets the current repeat mode for a guild.
+ *
+ * @param guild The guild
+ * @return The current RepeatMode
+ */
+ public RepeatMode getRepeatMode(Guild guild)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
+ return getSettings(guild).getRepeatMode();
+ }
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
+ /**
+ * Sets the repeat mode for a guild.
+ *
+ * @param guild The guild
+ * @param mode The repeat mode to set
+ */
+ public void setRepeatMode(Guild guild, RepeatMode mode)
+ {
+ LOG.info("Repeat mode changed: guild={}, mode={}", guild.getId(), mode.getUserFriendlyName());
+ getSettings(guild).setRepeatMode(mode);
+ }
+
+ public void adjustVolume(Guild guild, Member member, int change, OutputAdapter output)
+ {
+ if (!requireDJPermission(guild, member, output, "use this button"))
return;
- }
+
+ AudioHandler handler = getHandler(guild);
int newVol = handler.getPlayer().getVolume() + change;
newVol = Math.max(0, Math.min(150, newVol));
handler.getPlayer().setVolume(newVol);
- bot.getSettingsManager().getSettings(guild).setVolume(newVol);
+ getSettings(guild).setVolume(newVol);
output.editNowPlaying(handler);
}
- public void stop(Guild guild, Member member, OutputAdapter output)
+ /**
+ * Gets the current volume for a guild.
+ *
+ * @param guild The guild
+ * @return The current volume (0-150)
+ */
+ public int getVolume(Guild guild)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
+ AudioHandler handler = getHandler(guild);
+ return handler.getPlayer().getVolume();
+ }
+
+ /**
+ * Sets the volume to an absolute value.
+ *
+ * @param guild The guild
+ * @param volume The new volume (0-150)
+ * @return VolumeResult containing the old and new volume, or null if invalid
+ */
+ public VolumeResult setVolume(Guild guild, int volume)
+ {
+ LOG.debug("Volume change requested: guild={}, volume={}", guild.getId(), volume);
- if (!isDJ)
+ if (volume < 0 || volume > 150)
{
- output.replyError("You need to be a DJ to use this button!");
- return;
+ LOG.warn("Volume change rejected: invalid value {} (must be 0-150)", volume);
+ return null;
}
+
+ AudioHandler handler = getHandler(guild);
+ int oldVolume = handler.getPlayer().getVolume();
+ handler.getPlayer().setVolume(volume);
+ getSettings(guild).setVolume(volume);
+
+ LOG.info("Volume changed: guild={}, oldVolume={}, newVolume={}", guild.getId(), oldVolume, volume);
+
+ return new VolumeResult(oldVolume, volume);
+ }
+
+ /**
+ * Result of a volume change operation.
+ */
+ public static class VolumeResult
+ {
+ public final int oldVolume;
+ public final int newVolume;
+
+ public VolumeResult(int oldVolume, int newVolume)
+ {
+ this.oldVolume = oldVolume;
+ this.newVolume = newVolume;
+ }
+ }
+
+ public void stop(Guild guild, Member member, OutputAdapter output)
+ {
+ if (!requireDJPermission(guild, member, output, "use this button"))
+ return;
+
+ AudioHandler handler = getHandler(guild);
handler.stopAndClear();
guild.getAudioManager().closeAudioConnection();
output.editNoMusic(handler);
}
- public void pause(Guild guild, Member member, OutputAdapter output)
+ /**
+ * Stops playback and clears the queue (simple version without permission check).
+ * Use this when DJ permission is already verified by the caller.
+ *
+ * @param guild The guild
+ */
+ public void stopAndClear(Guild guild)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
+ LOG.info("Stopping playback and clearing queue: guild={}", guild.getId());
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to use this button!");
+ AudioHandler handler = getHandler(guild);
+ handler.stopAndClear();
+ guild.getAudioManager().closeAudioConnection();
+
+ LOG.debug("Audio connection closed: guild={}", guild.getId());
+ }
+
+ public void pause(Guild guild, Member member, OutputAdapter output)
+ {
+ if (!requireDJPermission(guild, member, output, "use this button"))
return;
- }
+
+ AudioHandler handler = getHandler(guild);
handler.getPlayer().setPaused(!handler.getPlayer().isPaused());
output.editNowPlaying(handler);
}
+ /**
+ * Checks if the player is currently paused.
+ *
+ * @param guild The guild
+ * @return true if paused, false otherwise
+ */
+ public boolean isPaused(Guild guild)
+ {
+ AudioHandler handler = getHandler(guild);
+ return handler.getPlayer().isPaused();
+ }
+
+ /**
+ * Sets the paused state of the player.
+ *
+ * @param guild The guild
+ * @param paused true to pause, false to resume
+ * @return The title of the currently playing track, or null if nothing is playing
+ */
+ public String setPaused(Guild guild, boolean paused)
+ {
+ AudioHandler handler = getHandler(guild);
+ handler.getPlayer().setPaused(paused);
+ AudioTrack track = handler.getPlayer().getPlayingTrack();
+ String trackTitle = track != null ? track.getInfo().title : null;
+
+ LOG.info("Player {} : guild={}, track=\"{}\"",
+ paused ? "paused" : "resumed", guild.getId(), trackTitle);
+
+ return trackTitle;
+ }
+
public void skip(Guild guild, Member member, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
RequestMetadata skipRm = handler.getRequestMetadata();
@@ -300,7 +562,7 @@ public void skip(Guild guild, Member member, OutputAdapter output)
output.replyError("You need to be a DJ or the requester to skip!");
return;
}
- if (bot.getSettingsManager().getSettings(guild).getRepeatMode() == RepeatMode.ALL)
+ if (getSettings(guild).getRepeatMode() == RepeatMode.ALL)
{
var track = handler.getPlayer().getPlayingTrack();
if (track != null)
@@ -311,12 +573,60 @@ public void skip(Guild guild, Member member, OutputAdapter output)
output.replySuccess("Skipped!");
}
+ /**
+ * Force skips the currently playing track (no permission check).
+ * Use this when DJ permission is already verified by the caller.
+ *
+ * @param guild The guild
+ * @return ForceSkipResult containing track info and requester, or null if nothing playing
+ */
+ public ForceSkipResult forceSkip(Guild guild)
+ {
+ LOG.debug("Force skip requested: guild={}", guild.getId());
+
+ AudioHandler handler = getHandler(guild);
+ AudioTrack track = handler.getPlayer().getPlayingTrack();
+ if (track == null)
+ {
+ LOG.debug("Force skip: nothing playing in guild={}", guild.getId());
+ return null;
+ }
+
+ RequestMetadata rm = handler.getRequestMetadata();
+ String trackTitle = track.getInfo().title;
+ String requesterInfo = rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)";
+
+ handler.getPlayer().stopTrack();
+
+ LOG.info("Track force-skipped: guild={}, track=\"{}\"", guild.getId(), trackTitle);
+
+ return new ForceSkipResult(trackTitle, requesterInfo);
+ }
+
+ /**
+ * Result of a force skip operation.
+ */
+ public static class ForceSkipResult
+ {
+ public final String trackTitle;
+ public final String requesterInfo;
+
+ public ForceSkipResult(String trackTitle, String requesterInfo)
+ {
+ this.trackTitle = trackTitle;
+ this.requesterInfo = requesterInfo;
+ }
+ }
+
public void skipWithVote(Guild guild, Member member, int listeners, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ LOG.debug("Skip vote requested: guild={}, user={}, listeners={}",
+ guild.getId(), member.getUser().getName(), listeners);
+
+ AudioHandler handler = getHandler(guild);
RequestMetadata rm = handler.getRequestMetadata();
- double skipRatio = bot.getSettingsManager().getSettings(guild).getSkipRatio();
+ double skipRatio = getSettings(guild).getSkipRatio();
if (skipRatio == -1)
{
skipRatio = bot.getConfig().getSkipRatio();
@@ -324,8 +634,11 @@ public void skipWithVote(Guild guild, Member member, int listeners, OutputAdapte
if (member.getIdLong() == rm.getOwner() || skipRatio == 0)
{
+ String trackTitle = handler.getPlayer().getPlayingTrack().getInfo().title;
handler.getPlayer().stopTrack();
- output.replySuccess("Skipped **" + handler.getPlayer().getPlayingTrack().getInfo().title + "**");
+ LOG.info("Track skipped by owner/instant skip: guild={}, user={}, track=\"{}\"",
+ guild.getId(), member.getUser().getName(), trackTitle);
+ output.replySuccess("Skipped **" + trackTitle + "**");
return;
}
@@ -348,6 +661,7 @@ public void skipWithVote(Guild guild, Member member, int listeners, OutputAdapte
if (alreadyVoted)
{
+ LOG.debug("Duplicate skip vote: guild={}, user={}", guild.getId(), member.getUser().getName());
output.replyWarning("You already voted to skip this song `" + voteStatus + "`");
}
else if (skippers >= required)
@@ -355,27 +669,36 @@ else if (skippers >= required)
String trackTitle = handler.getPlayer().getPlayingTrack().getInfo().title;
String requester = rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)";
handler.getPlayer().stopTrack();
+ LOG.info("Track skipped by vote: guild={}, track=\"{}\", votes={}/{}",
+ guild.getId(), trackTitle, skippers, required);
output.replySuccess("You voted to skip the song `" + voteStatus + "`\nSkipped **" + trackTitle + "** " + requester);
}
else
{
+ LOG.debug("Skip vote registered: guild={}, user={}, votes={}/{}",
+ guild.getId(), member.getUser().getName(), skippers, required);
output.replySuccess("You voted to skip the song `" + voteStatus + "`");
}
}
public void seek(Guild guild, Member member, String timeString, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ LOG.debug("Seek requested: guild={}, user={}, time={}",
+ guild.getId(), member.getUser().getName(), timeString);
+
+ AudioHandler handler = getHandler(guild);
AudioTrack playingTrack = handler.getPlayer().getPlayingTrack();
if (playingTrack == null)
{
+ LOG.debug("Seek rejected: no track playing in guild={}", guild.getId());
output.replyError("There is no track currently playing!");
return;
}
if (!playingTrack.isSeekable())
{
+ LOG.debug("Seek rejected: track not seekable - track=\"{}\"", playingTrack.getInfo().title);
output.replyError("This track is not seekable.");
return;
}
@@ -384,6 +707,8 @@ public void seek(Guild guild, Member member, String timeString, OutputAdapter ou
RequestMetadata rm = playingTrack.getUserData(RequestMetadata.class);
if (!isDJ && (rm == null || rm.getOwner() != member.getIdLong()))
{
+ LOG.debug("Seek rejected: user lacks permission - user={}, track=\"{}\"",
+ member.getUser().getName(), playingTrack.getInfo().title);
output.replyError("You cannot seek **" + playingTrack.getInfo().title + "** because you didn't add it!");
return;
}
@@ -391,6 +716,7 @@ public void seek(Guild guild, Member member, String timeString, OutputAdapter ou
TimeUtil.SeekTime seekTime = TimeUtil.parseTime(timeString);
if (seekTime == null)
{
+ LOG.debug("Seek rejected: invalid time format - input=\"{}\"", timeString);
output.replyError("Invalid seek! Expected format: [+ | -] or <0h0m0s>\nExamples: `1:02:23` `+1:10` `-90`, `1h10m`, `+90s`");
return;
}
@@ -405,6 +731,8 @@ public void seek(Guild guild, Member member, String timeString, OutputAdapter ou
}
if (seekMilliseconds > trackDuration)
{
+ LOG.debug("Seek rejected: position {} exceeds track duration {}",
+ TimeUtil.formatTime(seekMilliseconds), TimeUtil.formatTime(trackDuration));
output.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!");
return;
}
@@ -412,10 +740,15 @@ public void seek(Guild guild, Member member, String timeString, OutputAdapter ou
try
{
playingTrack.setPosition(seekMilliseconds);
+ LOG.info("Seek successful: guild={}, user={}, track=\"{}\", position={}",
+ guild.getId(), member.getUser().getName(), playingTrack.getInfo().title,
+ TimeUtil.formatTime(playingTrack.getPosition()));
output.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(trackDuration) + "`!");
}
catch (Exception e)
{
+ LOG.error("Seek failed: guild={}, track=\"{}\", error={}",
+ guild.getId(), playingTrack.getInfo().title, e.getMessage(), e);
output.replyError("An error occurred while trying to seek: " + e.getMessage());
}
}
@@ -424,19 +757,13 @@ public void seek(Guild guild, Member member, String timeString, OutputAdapter ou
public void removeTrack(Guild guild, Member member, int position, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
- if (handler.getQueue().isEmpty())
- {
- output.replyError("There is nothing in the queue!");
+ if (!requireNonEmptyQueue(handler, output))
return;
- }
- if (position < 1 || position > handler.getQueue().size())
- {
- output.replyError("Position must be a valid integer between 1 and " + handler.getQueue().size() + "!");
+ if (!validateQueuePosition(handler, position, output))
return;
- }
boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
QueuedTrack qt = handler.getQueue().get(position - 1);
@@ -467,13 +794,10 @@ else if (isDJ)
public void removeAllTracks(Guild guild, Member member, OutputAdapter output)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
- if (handler.getQueue().isEmpty())
- {
- output.replyError("There is nothing in the queue!");
+ if (!requireNonEmptyQueue(handler, output))
return;
- }
int count = handler.getQueue().removeAll(member.getIdLong());
if (count == 0)
@@ -486,14 +810,41 @@ public void removeAllTracks(Guild guild, Member member, OutputAdapter output)
}
}
+ /**
+ * Removes all tracks from a specific user (for DJ force remove).
+ *
+ * @param guild The guild
+ * @param userId The user ID whose tracks to remove
+ * @return The number of tracks removed
+ */
+ public int removeAllTracksByUser(Guild guild, long userId)
+ {
+ LOG.debug("Removing all tracks by user: guild={}, userId={}", guild.getId(), userId);
+
+ AudioHandler handler = getHandler(guild);
+ int count = handler.getQueue().removeAll(userId);
+
+ LOG.info("Removed {} tracks by user: guild={}, userId={}", count, guild.getId(), userId);
+
+ return count;
+ }
+
+ /**
+ * Checks if the queue is empty.
+ *
+ * @param guild The guild
+ * @return true if the queue is empty
+ */
+ public boolean isQueueEmpty(Guild guild)
+ {
+ AudioHandler handler = getHandler(guild);
+ return handler == null || handler.getQueue().isEmpty();
+ }
+
public void moveTrack(Guild guild, Member member, int from, int to, OutputAdapter output)
{
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
- if (!isDJ)
- {
- output.replyError("You need to be a DJ to move tracks!");
+ if (!requireDJPermission(guild, member, output, "move tracks"))
return;
- }
if (from == to)
{
@@ -501,7 +852,7 @@ public void moveTrack(Guild guild, Member member, int from, int to, OutputAdapte
return;
}
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
AbstractQueue queue = handler.getQueue();
if (isInvalidPosition(queue, from))
@@ -520,22 +871,60 @@ public void moveTrack(Guild guild, Member member, int from, int to, OutputAdapte
output.replySuccess("Moved **" + trackTitle + "** from position `" + from + "` to `" + to + "`.");
}
- public void skipTo(Guild guild, Member member, int position, OutputAdapter output)
+ /**
+ * Moves a track from one position to another (no permission check).
+ * Use this when DJ permission is already verified by the caller.
+ *
+ * @param guild The guild
+ * @param from The 1-based source position
+ * @param to The 1-based destination position
+ * @return The title of the moved track, or null if invalid positions
+ */
+ public String moveTrackPosition(Guild guild, int from, int to)
{
- boolean isDJ = DJCommand.checkDJPermission(bot, guild, member);
- if (!isDJ)
+ LOG.debug("Moving track: guild={}, from={}, to={}", guild.getId(), from, to);
+
+ AudioHandler handler = getHandler(guild);
+ AbstractQueue queue = handler.getQueue();
+
+ if (isInvalidPosition(queue, from) || isInvalidPosition(queue, to))
{
- output.replyError("You need to be a DJ to skip to a specific position!");
- return;
+ LOG.debug("Move rejected: invalid position(s) - from={}, to={}, queueSize={}",
+ from, to, queue.size());
+ return null;
}
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ QueuedTrack track = queue.moveItem(from - 1, to - 1);
+ String title = track.getTrack().getInfo().title;
- if (position < 1 || position > handler.getQueue().size())
- {
- output.replyError("Position must be a valid integer between 1 and " + handler.getQueue().size() + "!");
+ LOG.info("Track moved: guild={}, track=\"{}\", from={}, to={}",
+ guild.getId(), title, from, to);
+
+ return title;
+ }
+
+ /**
+ * Checks if a position is valid in the queue.
+ *
+ * @param guild The guild
+ * @param position The 1-based position to check
+ * @return true if the position is valid
+ */
+ public boolean isValidQueuePosition(Guild guild, int position)
+ {
+ AudioHandler handler = getHandler(guild);
+ return handler != null && position >= 1 && position <= handler.getQueue().size();
+ }
+
+ public void skipTo(Guild guild, Member member, int position, OutputAdapter output)
+ {
+ if (!requireDJPermission(guild, member, output, "skip to a specific position"))
+ return;
+
+ AudioHandler handler = getHandler(guild);
+
+ if (!validateQueuePosition(handler, position, output))
return;
- }
handler.getQueue().skip(position - 1);
String trackTitle = handler.getQueue().get(0).getTrack().getInfo().title;
@@ -543,18 +932,105 @@ public void skipTo(Guild guild, Member member, int position, OutputAdapter outpu
output.replySuccess("Skipped to **" + trackTitle + "**");
}
+ /**
+ * Skips to a specific position in the queue (no permission check).
+ * Use this when DJ permission is already verified by the caller.
+ *
+ * @param guild The guild
+ * @param position The 1-based position to skip to
+ * @return The title of the track skipped to, or null if invalid position
+ */
+ public String skipToPosition(Guild guild, int position)
+ {
+ LOG.debug("Skip to position: guild={}, position={}", guild.getId(), position);
+
+ AudioHandler handler = getHandler(guild);
+ int queueSize = handler.getQueue().size();
+
+ if (position < 1 || position > queueSize)
+ {
+ LOG.debug("Skip to position rejected: invalid position {} (queueSize={})",
+ position, queueSize);
+ return null;
+ }
+
+ handler.getQueue().skip(position - 1);
+ String trackTitle = handler.getQueue().get(0).getTrack().getInfo().title;
+ handler.getPlayer().stopTrack();
+
+ LOG.info("Skipped to position: guild={}, position={}, track=\"{}\"",
+ guild.getId(), position, trackTitle);
+
+ return trackTitle;
+ }
+
+ /**
+ * Gets the current queue size.
+ *
+ * @param guild The guild
+ * @return The number of tracks in the queue
+ */
+ public int getQueueSize(Guild guild)
+ {
+ AudioHandler handler = getHandler(guild);
+ return handler != null ? handler.getQueue().size() : 0;
+ }
+
+ // ========== Now Playing ==========
+
+ /**
+ * Gets the now playing message for a guild.
+ *
+ * @param guild The guild
+ * @param jda The JDA instance
+ * @return NowPlayingInfo containing the message data, or null if no handler
+ */
+ public NowPlayingInfo getNowPlayingInfo(Guild guild, JDA jda)
+ {
+ AudioHandler handler = getHandler(guild);
+ if (handler == null)
+ {
+ return null;
+ }
+
+ return new NowPlayingInfo(
+ handler.getNowPlaying(jda),
+ handler.getNoMusicPlaying(jda),
+ handler.getPlayer().getPlayingTrack() != null
+ );
+ }
+
+ /**
+ * Data class containing now playing information.
+ */
+ public static class NowPlayingInfo
+ {
+ public final net.dv8tion.jda.api.utils.messages.MessageCreateData nowPlayingMessage;
+ public final net.dv8tion.jda.api.utils.messages.MessageCreateData noMusicMessage;
+ public final boolean isPlaying;
+
+ public NowPlayingInfo(net.dv8tion.jda.api.utils.messages.MessageCreateData nowPlayingMessage,
+ net.dv8tion.jda.api.utils.messages.MessageCreateData noMusicMessage,
+ boolean isPlaying)
+ {
+ this.nowPlayingMessage = nowPlayingMessage;
+ this.noMusicMessage = noMusicMessage;
+ this.isPlaying = isPlaying;
+ }
+ }
+
// ========== Queue Info ==========
public QueueInfo getQueueInfo(Guild guild, JDA jda)
{
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
+ AudioHandler handler = getHandler(guild);
if (handler == null)
{
return null;
}
List list = handler.getQueue().getList();
- Settings settings = bot.getSettingsManager().getSettings(guild);
+ Settings settings = getSettings(guild);
long totalDuration = 0;
String[] trackStrings = new String[list.size()];
@@ -602,6 +1078,42 @@ private boolean isInvalidPosition(AbstractQueue queue, int position
return position < 1 || position > queue.size();
}
+ /**
+ * Validates a queue position and sends an error message if invalid.
+ *
+ * @param handler The audio handler
+ * @param position The 1-based position to validate
+ * @param output The output adapter for error messages
+ * @return true if the position is valid, false otherwise
+ */
+ private boolean validateQueuePosition(AudioHandler handler, int position, OutputAdapter output)
+ {
+ int size = handler.getQueue().size();
+ if (position < 1 || position > size)
+ {
+ output.replyError("Position must be a valid integer between 1 and " + size + "!");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the queue is non-empty and sends an error message if empty.
+ *
+ * @param handler The audio handler
+ * @param output The output adapter for error messages
+ * @return true if the queue is non-empty, false otherwise
+ */
+ private boolean requireNonEmptyQueue(AudioHandler handler, OutputAdapter output)
+ {
+ if (handler.getQueue().isEmpty())
+ {
+ output.replyError("There is nothing in the queue!");
+ return false;
+ }
+ return true;
+ }
+
// ========== Inner Classes ==========
/**
@@ -676,153 +1188,4 @@ public interface OutputAdapter
void editNoMusic(AudioHandler handler);
void onShowHelp();
}
-
- private class ResultHandler implements AudioLoadResultHandler
- {
- private final static String LOAD = "\uD83D\uDCE5"; // 📥
- private final static String CANCEL = "\uD83D\uDEAB"; // 🚫
-
- private final OutputAdapter output;
- private final Guild guild;
- private final Member member;
- private final String args;
- private final boolean ytsearch;
- private final TextChannel channel;
-
- private ResultHandler(OutputAdapter output, Guild guild, Member member, String args, boolean ytsearch, TextChannel channel)
- {
- this.output = output;
- this.guild = guild;
- this.member = member;
- this.args = args;
- this.ytsearch = ytsearch;
- this.channel = channel;
- }
-
- private void loadSingle(AudioTrack track, AudioPlaylist playlist)
- {
- TrackAddResult result = addTrackToQueue(guild, member, track, args, channel);
- if (result == null)
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " " + formatTooLongError(track)));
- return;
- }
-
- String addMsg = FormatUtil.filter(bot.getConfig().getSuccess() + " " + result.formattedMessage);
- if (playlist == null || !guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_ADD_REACTION))
- output.editMessage(addMsg);
- else
- {
- String promptMsg = addMsg + "\n" + bot.getConfig().getWarning() + " This track has a playlist of **" + playlist.getTracks().size() + "** tracks attached. Select " + LOAD + " to load playlist.";
-
- MessageEditBuilder editBuilder = new MessageEditBuilder()
- .setContent(promptMsg)
- .setComponents(ActionRow.of(
- Button.success("load_playlist", Emoji.fromUnicode(LOAD)).withLabel("Load Playlist"),
- Button.danger("cancel_playlist", Emoji.fromUnicode(CANCEL)).withLabel("Cancel")
- ));
-
- output.editMessage(addMsg, m -> {
- m.editMessage(editBuilder.build()).queue(msg -> {
- bot.getWaiter().waitForEvent(ButtonInteractionEvent.class,
- event -> event.getMessageId().equals(msg.getId()) &&
- (event.getComponentId().equals("load_playlist") || event.getComponentId().equals("cancel_playlist")) &&
- event.getUser().getIdLong() == member.getIdLong(),
- event -> {
- if (event.getComponentId().equals("load_playlist"))
- {
- int loaded = loadPlaylist(playlist, track);
- event.editMessage(addMsg + "\n" + bot.getConfig().getSuccess() + " Loaded **" + loaded + "** additional tracks!").setComponents().queue();
- }
- else
- {
- event.editMessage(addMsg).setComponents().queue();
- }
- },
- 30, TimeUnit.SECONDS,
- () -> msg.editMessage(addMsg).setComponents().queue());
- });
- });
- }
- }
-
- private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude)
- {
- int[] count = {0};
- AudioHandler handler = (AudioHandler) guild.getAudioManager().getSendingHandler();
- playlist.getTracks().forEach((track) -> {
- if (!isTooLong(track) && !track.equals(exclude))
- {
- handler.setLastReason(member.getUser().getName() + " added a playlist.");
- handler.addTrack(new QueuedTrack(track,
- new RequestMetadata(member.getUser(),
- new RequestMetadata.RequestInfo(args, track.getInfo().uri),
- channel.getIdLong())));
- count[0]++;
- }
- });
- return count[0];
- }
-
- @Override
- public void trackLoaded(AudioTrack track)
- {
- loadSingle(track, null);
- }
-
- @Override
- public void playlistLoaded(AudioPlaylist playlist)
- {
- if (playlist.getTracks().size() == 1 || playlist.isSearchResult())
- {
- AudioTrack single = playlist.getSelectedTrack() == null ? playlist.getTracks().get(0) : playlist.getSelectedTrack();
- loadSingle(single, null);
- }
- else if (playlist.getSelectedTrack() != null)
- {
- AudioTrack single = playlist.getSelectedTrack();
- loadSingle(single, playlist);
- }
- else
- {
- int count = loadPlaylist(playlist, null);
- if (playlist.getTracks().size() == 0)
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " The playlist " + (playlist.getName() == null ? "" : "(**" + playlist.getName()
- + "**) ") + " could not be loaded or contained 0 entries"));
- }
- else if (count == 0)
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " All entries in this playlist " + (playlist.getName() == null ? "" : "(**" + playlist.getName()
- + "**) ") + "were longer than the allowed maximum (`" + bot.getConfig().getMaxTime() + "`)"));
- }
- else
- {
- output.editMessage(FormatUtil.filter(bot.getConfig().getSuccess() + " Found "
- + (playlist.getName() == null ? "a playlist" : "playlist **" + playlist.getName() + "**") + " with `"
- + playlist.getTracks().size() + "` entries; added to the queue!"
- + (count < playlist.getTracks().size() ? "\n" + bot.getConfig().getWarning() + " Tracks longer than the allowed maximum (`"
- + bot.getConfig().getMaxTime() + "`) have been omitted." : "")));
- }
- }
- }
-
- @Override
- public void noMatches()
- {
- if (ytsearch)
- output.editMessage(FormatUtil.filter(bot.getConfig().getWarning() + " No results found for `" + args + "`."));
- else
- bot.getPlayerManager().loadItemOrdered(guild, "ytsearch:" + args, new ResultHandler(output, guild, member, args, true, channel));
- }
-
- @Override
- public void loadFailed(FriendlyException throwable)
- {
- if (throwable.severity == Severity.COMMON)
- output.editMessage(bot.getConfig().getError() + " Error loading: " + throwable.getMessage());
- else
- output.editMessage(bot.getConfig().getError() + " Error loading track.");
- }
- }
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/service/SearchService.java b/src/main/java/com/jagrosh/jmusicbot/service/SearchService.java
index 2cb9c20b0..e0d04d721 100644
--- a/src/main/java/com/jagrosh/jmusicbot/service/SearchService.java
+++ b/src/main/java/com/jagrosh/jmusicbot/service/SearchService.java
@@ -25,6 +25,8 @@
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.List;
@@ -34,11 +36,14 @@
*/
public class SearchService
{
+ private static final Logger LOG = LoggerFactory.getLogger(SearchService.class);
+
private final Bot bot;
public SearchService(Bot bot)
{
this.bot = bot;
+ LOG.info("SearchService initialized");
}
/**
@@ -54,12 +59,19 @@ public SearchService(Bot bot)
public void search(Guild guild, Member member, String query, String searchPrefix,
TextChannel channel, SearchCallback callback)
{
+ LOG.debug("Search requested: guild={}, user={}, query=\"{}\", prefix={}",
+ guild.getId(), member.getUser().getName(), query, searchPrefix);
+
if (query == null || query.isEmpty())
{
+ LOG.debug("Search rejected: empty query");
callback.onError("Please include a query.");
return;
}
+ LOG.info("Executing search: guild={}, user={}, query=\"{}\"",
+ guild.getId(), member.getUser().getName(), query);
+
bot.getPlayerManager().loadItemOrdered(guild, searchPrefix + query,
new SearchResultHandler(guild, member, query, channel, callback));
}
@@ -136,22 +148,32 @@ private SearchResultHandler(Guild guild, Member member, String query,
@Override
public void trackLoaded(AudioTrack track)
{
+ LOG.debug("Search result - single track: guild={}, track=\"{}\"",
+ guild.getId(), track.getInfo().title);
+
MusicService musicService = bot.getMusicService();
// Use shared track operations from MusicService
MusicService.TrackAddResult result = musicService.addTrackToQueue(guild, member, track, query, channel);
if (result == null)
{
+ LOG.warn("Search track rejected (too long): guild={}, track=\"{}\"",
+ guild.getId(), track.getInfo().title);
callback.onLoadFailed(musicService.formatTooLongError(track));
return;
}
+ LOG.info("Search result added to queue: guild={}, track=\"{}\", position={}",
+ guild.getId(), track.getInfo().title, result.position);
callback.onTrackLoaded(track, result.position, result.formattedMessage);
}
@Override
public void playlistLoaded(AudioPlaylist playlist)
{
+ LOG.debug("Search results loaded: guild={}, query=\"{}\", results={}",
+ guild.getId(), query, playlist.getTracks().size());
+
String[] choices = formatSearchChoices(playlist.getTracks(), 4);
callback.onSearchResults(playlist, choices);
}
@@ -159,6 +181,7 @@ public void playlistLoaded(AudioPlaylist playlist)
@Override
public void noMatches()
{
+ LOG.debug("Search - no matches: guild={}, query=\"{}\"", guild.getId(), query);
callback.onNoMatches(query);
}
@@ -167,10 +190,14 @@ public void loadFailed(FriendlyException throwable)
{
if (throwable.severity == Severity.COMMON)
{
+ LOG.warn("Search load failed (common): guild={}, query=\"{}\", error={}",
+ guild.getId(), query, throwable.getMessage());
callback.onLoadFailed("Error loading: " + throwable.getMessage());
}
else
{
+ LOG.error("Search load failed (severe): guild={}, query=\"{}\"",
+ guild.getId(), query, throwable);
callback.onLoadFailed("Error loading track.");
}
}
From 6e9601ee786158311a15dae0e3a8352d927a2aa4 Mon Sep 17 00:00:00 2001
From: Arif Banai <6625454+arif-banai@users.noreply.github.com>
Date: Sun, 25 Jan 2026 22:17:46 -0500
Subject: [PATCH 22/24] Refactor SearchSlashCmd to utilize SearchService
---
.../jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
index 9503e2b8b..6766ec833 100644
--- a/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
+++ b/src/main/java/com/jagrosh/jmusicbot/commands/v2/music/SearchSlashCmd.java
@@ -66,8 +66,8 @@ public void doCommand(SlashCommandEvent event)
event.reply(searchingEmoji + " Searching " + searchPlatform + " for `" + query + "`...").queue(hook ->
{
- bot.getPlayerManager().loadItemOrdered(event.getGuild(), searchPrefix + query,
- new SearchService.SearchCallback()
+ bot.getSearchService().search(event.getGuild(), event.getMember(), query, searchPrefix,
+ event.getTextChannel(), new SearchService.SearchCallback()
{
@Override
public void onTrackLoaded(AudioTrack track, int queuePosition, String formattedMessage)
From 8a23b34aa19ebb9958737f39b858f46867e16bcf Mon Sep 17 00:00:00 2001
From: Arif Banai <6625454+arif-banai@users.noreply.github.com>
Date: Tue, 27 Jan 2026 01:55:23 -0500
Subject: [PATCH 23/24] Update README.md to reflect system requirements and
clarify Java version dependencies
- Specified that glibc version must be 2.38 or higher for proper functionality.
- Revised instructions for running the bot directly and clarified native library installation steps.
- Enhanced documentation for Docker usage and configuration persistence.
---
README.md | 52 ++++++++++++++++++++++++++--------------------------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index f30461e7e..e340d4e27 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,10 @@ A cross-platform Discord music bot with a clean interface, and that is easy to s
This version of JMusicBot changes/updates various dependencies. To ensure your bot continues to function correctly, please note the following mandatory changes:
-* **Java 25 Minimum:** The bot now requires **Java 25 or higher**. Please update your hosting environment (check `java -version`) before running the new JAR.
-* **LibDave/udpqueue:** You **must** install the required native libraries for your operating system. If you are using Docker, this is already handled for you.
+* **Java 25 Minimum:** The bot now requires **Java 25 or higher**.
+
+* **LibDave/udpqueue:** You **must** have glibc >= 2.38. **If you are using Docker, this is already handled for you.**
+
* **Privileged Gateway Intents:** You **must** enable the **Message Content Intent** in your [Discord Developer Portal](https://discord.com/developers/applications).
* *Navigate to: Your Application > Bot > Privileged Gateway Intents > Toggle "Message Content Intent" to ON.*
* *Without this, the bot will not see your commands.*
@@ -69,11 +71,7 @@ Please see the [Setup Page](https://jmusicbot.com/setup) to run this bot yoursel
## Running Directly (Without Docker)
-When running JMusicBot directly (not in Docker), you need to use specific JVM flags for Java 22+.
-
-### Required JVM Flags
-
-Java 22 and later require the `--enable-native-access=ALL-UNNAMED` flag to load native libraries (JDave for Discord voice encryption, udpqueue for audio sending):
+When running JMusicBot directly (not in Docker), make sure to pass these JVM flags:
```bash
java -Dnogui=true --enable-native-access=ALL-UNNAMED -jar JMusicBot-0.6.2-All.jar
@@ -81,28 +79,25 @@ java -Dnogui=true --enable-native-access=ALL-UNNAMED -jar JMusicBot-0.6.2-All.ja
### Linux System Requirements
-On Debian/Ubuntu-based systems, you may need to install the following dependencies for native audio libraries:
+**Important:** Your system **must have glibc version 2.38 or higher**. Failure to meet this requirement will result in errors when using JDave and udpqueue.
+
+> **Note:** Ubuntu 24.04 "Noble" and Debian 13 "Trixie" already include a compatible glibc version out of the box.
+You can check your glibc version with:
```bash
-# Install required native library dependencies
-sudo apt-get update
-sudo apt-get install -y libopus0 libsodium23
+ldd --version
```
-### Using the Run Script
-
-The included `scripts/run_jmusicbot.sh` script handles the JVM flags automatically:
+On Debian/Ubuntu-based systems, you may also need to install the following native audio library dependencies:
```bash
-chmod +x scripts/run_jmusicbot.sh
-./scripts/run_jmusicbot.sh
+# Install required native library dependencies
+sudo apt-get update
+sudo apt-get install -y libopus0 libsodium23
```
-You can customize JVM options by setting the `JAVA_OPTS` environment variable:
-```bash
-JAVA_OPTS="--enable-native-access=ALL-UNNAMED -Xmx512m" ./scripts/run_jmusicbot.sh
-```
+If your version is below 2.38, you will need to upgrade your system or use a compatible runtime. (You can also use Docker, which is recommended.)
## Docker
@@ -164,16 +159,12 @@ services:
Check the [Docker Compose Example](docker-compose.example.yml) for more details.
-The Dockerfile uses a multi-stage build:
-- **Stage 1:** Builds the application with Maven (Java 25) - copies `pom.xml` first for better layer caching, then builds the shaded jar
-- **Stage 2:** Creates a minimal runtime image with `eclipse-temurin:25-jre-noble` - copies the built jar as `/app/app.jar` and sets up the entrypoint script
-
### Important Notes
- **Config Persistence:** The `/musicbot` volume **must** be mounted for your configuration to persist. The bot reads and writes `config.txt` from `/musicbot` (the container's working directory).
- **First Run:** If `config.txt` doesn't exist, the bot will generate a default one automatically. You'll need to edit it with your bot token before the bot can start.
- **Image Tags:**
- - Use `ghcr.io/arif-banai/musicbot:latest` for the latest build from the default branch
+ - Use `ghcr.io/arif-banai/musicbot:latest` for the latest build from the master branch
- Use `ghcr.io/arif-banai/musicbot:0.6.1` (replace with actual version) to pin a specific release version
- **Recommendation:** For production, pin your image tag rather than using `latest`
- **JAVA_OPTS:** You can optionally set `JAVA_OPTS` environment variable to pass additional JVM arguments (e.g., `-Xmx512m -Xms256m` for memory settings).
@@ -194,7 +185,16 @@ This project follows a **trunk-based development** workflow. The `master` branch
Branch names are automatically validated by CI to ensure consistency. For detailed information about the development workflow, branch naming rules, and best practices, see [DEVELOPMENT_WORKFLOW.md](docs/DEVELOPMENT_WORKFLOW.md).
## Questions/Suggestions/Bug Reports
-**Please read the [Issues List](https://github.com/arif-banai/MusicBot/issues) before suggesting a feature**. If you have a question, need troubleshooting help, or want to brainstorm a new feature, please start a [Discussion](https://github.com/arif-banai/MusicBot/discussions). If you'd like to suggest a feature or report a reproducible bug, please open an [Issue](https://github.com/arif-banai/MusicBot/issues) on this repository. If you like this bot, be sure to add a star to the libraries that make this possible: [**JDA**](https://github.com/DV8FromTheWorld/JDA) and [**lavaplayer**](https://github.com/lavalink-devs/lavaplayer)!
+**Please read the [Issues List](https://github.com/arif-banai/MusicBot/issues) before suggesting a feature**.
+
+If you have a question, need troubleshooting help, or want to brainstorm a new feature, please start a [Discussion](https://github.com/arif-banai/MusicBot/discussions).
+
+The Discord server is also available for questions and suggestions. [Click here to join](https://discord.gg/cyyUxNmmx6).
+
+ If you'd like to suggest a feature or report a reproducible bug, please open an [Issue](https://github.com/arif-banai/MusicBot/issues) on this repository. If you like this bot, be sure to add a star to the libraries that make this possible:
+ - [**JDA**](https://github.com/DV8FromTheWorld/JDA)
+ - [**lavaplayer**](https://github.com/lavalink-devs/lavaplayer)
+ - [**youtube-source**](https://github.com/lavalink-devs/youtube-source)
## Editing
This bot (and the source code here) might not be easy to edit for inexperienced programmers. The main purpose of having the source public is to show the capabilities of the libraries, to allow others to understand how the bot works, and to allow those knowledgeable about java, JDA, and Discord bot development to contribute. There are many requirements and dependencies required to edit and compile it, and there will not be support provided for people looking to make changes on their own. Instead, consider making a feature request (see the above section). If you choose to make edits, please do so in accordance with the Apache 2.0 License.
From f4004354be9970950381d32e9b55165b2212f9c5 Mon Sep 17 00:00:00 2001
From: Arif Banai <6625454+arif-banai@users.noreply.github.com>
Date: Tue, 27 Jan 2026 01:59:42 -0500
Subject: [PATCH 24/24] Update README.md to clarify system requirements and
correct lavaplayer link
- Corrected the link to the lavaplayer documentation for supported sources and formats.
---
README.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index e340d4e27..97dbe8e42 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,7 @@ This version of JMusicBot changes/updates various dependencies. To ensure your b
* **Java 25 Minimum:** The bot now requires **Java 25 or higher**.
-* **LibDave/udpqueue:** You **must** have glibc >= 2.38. **If you are using Docker, this is already handled for you.**
-
+* **LibDave/udpqueue:** You **must** have **glibc >= 2.38**. *If you are using Docker, this is already handled for you.*
* **Privileged Gateway Intents:** You **must** enable the **Message Content Intent** in your [Discord Developer Portal](https://discord.com/developers/applications).
* *Navigate to: Your Application > Bot > Privileged Gateway Intents > Toggle "Message Content Intent" to ON.*
* *Without this, the bot will not see your commands.*
@@ -44,7 +43,7 @@ This version of JMusicBot changes/updates various dependencies. To ensure your b
* Playlist support (both web/youtube, and local)
## Supported sources and formats
-JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/sedmelluq/lavaplayer#supported-formats):
+JMusicBot supports all sources and formats supported by [lavaplayer](https://github.com/lavalink-devs/lavaplayer#supported-formats):
### Sources
* YouTube
* SoundCloud