package moe.oko.Kiafumi.command.music; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import moe.oko.Kiafumi.command.CommandClass; import moe.oko.Kiafumi.model.audio.AudioInfo; import moe.oko.Kiafumi.model.audio.AudioPlayerSendHandler; import moe.oko.Kiafumi.model.audio.TrackManager; import moe.oko.Kiafumi.util.CommandInfo; import moe.oko.Kiafumi.util.CommandType; import moe.oko.Kiafumi.util.EmbedUI; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.commands.OptionType; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import static moe.oko.Kiafumi.util.LoggingManager.info; import static moe.oko.Kiafumi.util.LoggingManager.slashLog; /** * Music Command * Some code may be taken from SHIRO Project (ISC License still applies) * @author Kay, oko */ public class MusicCommand extends CommandClass { private final AudioPlayerManager audioPlayerManager = new DefaultAudioPlayerManager(); private final Map> players = new HashMap<>(); public MusicCommand() { AudioSourceManagers.registerRemoteSources(audioPlayerManager); } @Override public boolean isEnabled() { return true; } @Override public String getName() { return "Music"; } @Override public void newCommand(String name, SlashCommandInteractionEvent e) { switch (name) { case "play" -> { e.deferReply().queue(); var input = e.getOption("url").getAsString(); slashLog(e, "with search \"%s\".".formatted(input)); if(input.startsWith("https://")) loadTrack(input, e.getMember(), e.getHook()); else loadTrack("ytsearch: " + input, e.getMember(), e.getHook()); } case "skip" -> { e.deferReply().queue(); slashLog(e); if (isAdmin(e.getMember())) { getPlayer(e.getGuild()).stopTrack(); e.getHook().sendMessage("Skipping the current track.").queue(); } else { var info = getTrackManager(e.getGuild()).getTrackInfo(getPlayer(e.getGuild()).getPlayingTrack()); if (info.hasVoted(e.getUser())) e.getHook().setEphemeral(true).sendMessage("You've already voted to skip this track."); else { int votes = info.getSkips(); int users = info.getAuthor().getVoiceState().getChannel().getMembers().size()-1; int requiredVotes = users/2; if (votes > requiredVotes) { getPlayer(e.getGuild()).stopTrack(); e.getHook().sendMessage("Skipping the current track.").queue(); } else { info.addSkip(e.getUser()); e.getHook().sendMessage("**%s** has voted to skip the track (%s/%s)" .formatted(e.getUser().getName(), votes, requiredVotes)).queue(); } } } } case "nowplaying" -> { e.deferReply().queue(); slashLog(e); if (!hasPlayer(e.getGuild()) || getPlayer(e.getGuild()).getPlayingTrack() == null) e.getHook().sendMessage("No song is currently playing.").queue(); else { var track = getPlayer(e.getGuild()).getPlayingTrack().getInfo(); e.getHook().sendMessageEmbeds(new EmbedBuilder() .setColor(EmbedUI.SUCCESS) .setAuthor("Now playing") .setDescription("[%s](%s)".formatted(track.title, track.uri)) .addField("Info", "Channel: %s\nLength: %s".formatted(track.author, getTimestamp(track.length)), false) .setFooter("Requested by: " + e.getUser().getName()).build()).queue(); } } case "queue" -> { e.deferReply().queue(); slashLog(e); if (!hasPlayer(e.getGuild()) || getTrackManager(e.getGuild()).getQueuedTracks().isEmpty()) e.getHook().sendMessage("There is nothing queued.").queue(); else { var trackList = new StringBuilder(); var queuedTracks = getTrackManager(e.getGuild()).getQueuedTracks(); final short[] trackSize = {-1}; queuedTracks.forEach(audioInfo -> { trackList.append(buildQueueString(audioInfo)); trackSize[0]++; }); var eb = new EmbedBuilder().setColor(EmbedUI.SUCCESS); if (trackList.length() >= 990) { eb.addField("Queue", "**>** " + trackList.toString(), false); } else { eb.addField("Queue", "**>** " + getPlayer(e.getGuild()).getPlayingTrack().getInfo().title + "\n" + trackSize[0] + " other tracks..", false).build(); } e.getHook().sendMessageEmbeds(eb.build()).queue(); } } } } private void loadTrack(String input, Member author, InteractionHook hook) { if (author.getVoiceState().getChannel() == null) { hook.setEphemeral(true).sendMessage("You are not in a voice channel."); return; } var server = author.getGuild(); getPlayer(server); audioPlayerManager.loadItemOrdered(server, input, new AudioLoadResultHandler() { EmbedBuilder eb; @Override public void trackLoaded(AudioTrack audioTrack) { var trackInfo = audioTrack.getInfo(); eb = new EmbedBuilder() .setColor(EmbedUI.SUCCESS) .setAuthor("Playing") .setDescription("[%s](%s)".formatted(trackInfo.title, trackInfo.uri)) .addField("Info", "Channel: %s\nLength: %s".formatted(trackInfo.author, getTimestamp(trackInfo.length)), false) .setFooter("Requested by: " + author.getEffectiveName()); hook.sendMessageEmbeds(eb.build()).queue(); getTrackManager(server).queue(audioTrack, author); } @Override public void playlistLoaded(AudioPlaylist audioPlaylist) { if (input.startsWith("ytsearch:")) { getTrackManager(server).queue(audioPlaylist.getTracks().get(0), author); var trackInfo = audioPlaylist.getTracks().get(0).getInfo(); eb = new EmbedBuilder() .setColor(EmbedUI.SUCCESS) .setAuthor("Playing") .setDescription("[%s](%s)".formatted(trackInfo.title, trackInfo.uri)) .addField("Info", "Channel: %s\nLength: %s".formatted(trackInfo.author, getTimestamp(trackInfo.length)), false) .setFooter("Requested by: " + author.getEffectiveName()); } else { for (AudioTrack audioTrack : audioPlaylist.getTracks()) { getTrackManager(server).queue(audioTrack, author); eb = new EmbedBuilder() .setColor(EmbedUI.SUCCESS) .setAuthor("Loaded tracks") .setDescription("**%s** tracks added to the queue.".formatted(audioPlaylist.getTracks().size())) .setFooter("Requested by: " + author.getEffectiveName()); } } hook.sendMessageEmbeds(eb.build()).queue(); } @Override public void noMatches() { eb = new EmbedBuilder() .setColor(EmbedUI.FAILURE) .setAuthor("Error") .setDescription("No matches were found."); hook.sendMessageEmbeds(eb.build()).queue(); } @Override public void loadFailed(FriendlyException e) { hook.sendMessage("Unable to play track.").queue(); } }); } private TrackManager getTrackManager(Guild server) { return players.get(server.getId()).getValue(); } private AudioPlayer getPlayer(Guild server) { var player = hasPlayer(server) ? players.get(server.getId()).getKey() : createPlayer(server); return player; } private AudioPlayer createPlayer(Guild server) { var newPlayer = audioPlayerManager.createPlayer(); var manager = new TrackManager(newPlayer); newPlayer.addListener(manager); server.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(newPlayer)); players.put(server.getId(), new AbstractMap.SimpleEntry<>(newPlayer, manager)); return newPlayer; } private boolean hasPlayer(Guild server) { return players.containsKey(server.getId()); } private boolean isAdmin(Member member) { return member.hasPermission(Permission.ADMINISTRATOR); } private String getTimestamp(long ms) { return String.format("%02d:%02d:%02d", ms/(3600*1000), ms/(60*1000) % 60, ms/1000 % 60); } private String buildQueueString(AudioInfo info) { var trackInfo = info.getTrack().getInfo(); return "%s [%s]\n".formatted(trackInfo.title, getTimestamp(trackInfo.length)); } @Override public List getSlashCommandInfo() { List cil = new ArrayList<>(); var playCommandList = new CommandInfo("play", "Adds a new track to the queue.", CommandType.COMMAND); playCommandList.addOption("url", "The URL or title of the track you want to play.", OptionType.STRING, true); cil.add(playCommandList); cil.add(new CommandInfo("skip", "Votes to skip the current track.", CommandType.COMMAND)); cil.add(new CommandInfo("nowplaying", "Displays the currently played track." , CommandType.COMMAND)); cil.add(new CommandInfo("queue", "Displays the tracks currently queued.", CommandType.COMMAND)); return cil; } }