aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/soundbot/AudioPlayerSendHandler.java48
-rw-r--r--src/main/java/soundbot/GuildMusicManager.java35
-rw-r--r--src/main/java/soundbot/Main.java167
-rw-r--r--src/main/java/soundbot/TrackScheduler.java56
4 files changed, 306 insertions, 0 deletions
diff --git a/src/main/java/soundbot/AudioPlayerSendHandler.java b/src/main/java/soundbot/AudioPlayerSendHandler.java
new file mode 100644
index 0000000..6499aad
--- /dev/null
+++ b/src/main/java/soundbot/AudioPlayerSendHandler.java
@@ -0,0 +1,48 @@
+package soundbot;
+
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
+import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
+import net.dv8tion.jda.core.audio.AudioSendHandler;
+
+/**
+ * This is a wrapper around AudioPlayer which makes it behave as an AudioSendHandler for JDA. As JDA calls canProvide
+ * before every call to provide20MsAudio(), we pull the frame in canProvide() and use the frame we already pulled in
+ * provide20MsAudio().
+ */
+public class AudioPlayerSendHandler implements AudioSendHandler {
+ private final AudioPlayer audioPlayer;
+ private AudioFrame lastFrame;
+
+ /**
+ * @param audioPlayer Audio player to wrap.
+ */
+ public AudioPlayerSendHandler(AudioPlayer audioPlayer) {
+ this.audioPlayer = audioPlayer;
+ }
+
+ @Override
+ public boolean canProvide() {
+ if (lastFrame == null) {
+ lastFrame = audioPlayer.provide();
+ }
+
+ return lastFrame != null;
+ }
+
+ @Override
+ public byte[] provide20MsAudio() {
+ if (lastFrame == null) {
+ lastFrame = audioPlayer.provide();
+ }
+
+ byte[] data = lastFrame != null ? lastFrame.data : null;
+ lastFrame = null;
+
+ return data;
+ }
+
+ @Override
+ public boolean isOpus() {
+ return true;
+ }
+}
diff --git a/src/main/java/soundbot/GuildMusicManager.java b/src/main/java/soundbot/GuildMusicManager.java
new file mode 100644
index 0000000..fc46756
--- /dev/null
+++ b/src/main/java/soundbot/GuildMusicManager.java
@@ -0,0 +1,35 @@
+package soundbot;
+
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
+
+/**
+ * Holder for both the player and a track scheduler for one guild.
+ */
+public class GuildMusicManager {
+ /**
+ * Audio player for the guild.
+ */
+ public final AudioPlayer player;
+ /**
+ * Track scheduler for the player.
+ */
+ public final TrackScheduler scheduler;
+
+ /**
+ * Creates a player and a track scheduler.
+ * @param manager Audio player manager to use for creating the player.
+ */
+ public GuildMusicManager(AudioPlayerManager manager) {
+ player = manager.createPlayer();
+ scheduler = new TrackScheduler(player);
+ player.addListener(scheduler);
+ }
+
+ /**
+ * @return Wrapper around AudioPlayer to use it as an AudioSendHandler.
+ */
+ public AudioPlayerSendHandler getSendHandler() {
+ return new AudioPlayerSendHandler(player);
+ }
+}
diff --git a/src/main/java/soundbot/Main.java b/src/main/java/soundbot/Main.java
new file mode 100644
index 0000000..2255c68
--- /dev/null
+++ b/src/main/java/soundbot/Main.java
@@ -0,0 +1,167 @@
+package soundbot;
+
+import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
+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 net.dv8tion.jda.core.AccountType;
+import net.dv8tion.jda.core.JDA;
+import net.dv8tion.jda.core.JDABuilder;
+import net.dv8tion.jda.core.entities.Guild;
+import net.dv8tion.jda.core.entities.TextChannel;
+import net.dv8tion.jda.core.entities.VoiceChannel;
+import net.dv8tion.jda.core.events.message.MessageReceivedEvent;
+import net.dv8tion.jda.client.events.call.voice.CallVoiceJoinEvent;
+import net.dv8tion.jda.core.hooks.ListenerAdapter;
+import net.dv8tion.jda.core.managers.AudioManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Main extends ListenerAdapter {
+ public static void main(String[] args) throws Exception {
+ JDA jda = new JDABuilder(AccountType.BOT)
+ .setToken(System.getProperty("botToken"))
+ .buildBlocking();
+
+ jda.addEventListener(new Main());
+ }
+
+ private final AudioPlayerManager playerManager;
+ private final Map<Long, GuildMusicManager> musicManagers;
+
+ private Main() {
+ this.musicManagers = new HashMap<>();
+
+ this.playerManager = new DefaultAudioPlayerManager();
+ AudioSourceManagers.registerRemoteSources(playerManager);
+ AudioSourceManagers.registerLocalSource(playerManager);
+ }
+
+ private synchronized GuildMusicManager getGuildAudioPlayer(Guild guild) {
+ long guildId = Long.parseLong(guild.getId());
+ GuildMusicManager musicManager = musicManagers.get(guildId);
+
+ if (musicManager == null) {
+ musicManager = new GuildMusicManager(playerManager);
+ musicManagers.put(guildId, musicManager);
+ }
+
+ guild.getAudioManager().setSendingHandler(musicManager.getSendHandler());
+
+ return musicManager;
+ }
+
+ @Override
+ public void onCallVoiceJoin(CallVoiceJoinEvent event){
+
+ }
+
+ @Override
+ public void onMessageReceived(MessageReceivedEvent event) {
+ String[] command = event.getMessage().getContentRaw().split(" ", 2);
+ Guild guild = event.getGuild();
+
+ if (guild != null) {
+ if ("~play".equals(command[0]) && command.length == 2) {
+ loadAndPlay(event.getTextChannel(), command[1]);
+ } else if ("~skip".equals(command[0])) {
+ skipTrack(event.getTextChannel());
+ } else if ("~volume".equals(command[0]) && command.length == 2) {
+ changeVolume(event.getTextChannel(), command[1]);
+ } else if ("~pause".equals(command[0])) {
+ pauseTrack(event.getTextChannel());
+ } else if ("~unpause".equals(command[0])) {
+ unpauseTrack(event.getTextChannel());
+ }
+ }
+
+ super.onMessageReceived(event);
+ }
+
+ private void changeVolume(final TextChannel channel, final String volume) {
+ GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
+ musicManager.player.setVolume(Integer.parseInt(volume));
+ channel.sendMessage("Volume now set to " + volume + "%").queue();
+ }
+
+ private void pauseTrack(final TextChannel channel){
+ GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
+ musicManager.player.setPaused(true);
+ channel.sendMessage("Playback Paused.").queue();
+ }
+
+ private void unpauseTrack(final TextChannel channel){
+ GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
+ musicManager.player.setPaused(false);
+ channel.sendMessage("Unpaused playback.").queue();
+ }
+
+ private void loadAndPlay(final TextChannel channel, final String trackUrl) {
+ GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
+
+ playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() {
+ @Override
+ public void trackLoaded(AudioTrack track) {
+ int timeStart = trackUrl.lastIndexOf('=');
+ if(timeStart != -1){
+ String timeString = trackUrl.substring(timeStart);
+ //The format will be 1h2m53s, need to parse that into seconds and then call
+ //track.setPosition(long position)
+
+ }
+ channel.sendMessage("Adding to queue " + track.getInfo().title).queue();
+
+ play(channel.getGuild(), musicManager, track);
+ }
+
+ @Override
+ public void playlistLoaded(AudioPlaylist playlist) {
+ AudioTrack firstTrack = playlist.getSelectedTrack();
+
+ if (firstTrack == null) {
+ firstTrack = playlist.getTracks().get(0);
+ }
+
+ channel.sendMessage("Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")").queue();
+
+ play(channel.getGuild(), musicManager, firstTrack);
+ }
+
+ @Override
+ public void noMatches() {
+ channel.sendMessage("Nothing found by " + trackUrl).queue();
+ }
+
+ @Override
+ public void loadFailed(FriendlyException exception) {
+ channel.sendMessage("Could not play: " + exception.getMessage()).queue();
+ }
+ });
+ }
+
+ private void play(Guild guild, GuildMusicManager musicManager, AudioTrack track) {
+ connectToFirstVoiceChannel(guild.getAudioManager());
+
+ musicManager.scheduler.queue(track);
+ }
+
+ private void skipTrack(TextChannel channel) {
+ GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
+ musicManager.scheduler.nextTrack();
+
+ channel.sendMessage("Skipped to next track.").queue();
+ }
+
+ private static void connectToFirstVoiceChannel(AudioManager audioManager) {
+ if (!audioManager.isConnected() && !audioManager.isAttemptingToConnect()) {
+ for (VoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) {
+ audioManager.openAudioConnection(voiceChannel);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/soundbot/TrackScheduler.java b/src/main/java/soundbot/TrackScheduler.java
new file mode 100644
index 0000000..4cbda71
--- /dev/null
+++ b/src/main/java/soundbot/TrackScheduler.java
@@ -0,0 +1,56 @@
+package soundbot;
+
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
+import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
+import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
+import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * This class schedules tracks for the audio player. It contains the queue of tracks.
+ */
+public class TrackScheduler extends AudioEventAdapter {
+ private final AudioPlayer player;
+ private final BlockingQueue<AudioTrack> queue;
+
+ /**
+ * @param player The audio player this scheduler uses
+ */
+ public TrackScheduler(AudioPlayer player) {
+ this.player = player;
+ this.queue = new LinkedBlockingQueue<>();
+ }
+
+ /**
+ * Add the next track to queue or play right away if nothing is in the queue.
+ *
+ * @param track The track to play or add to queue.
+ */
+ public void queue(AudioTrack track) {
+ // Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If
+ // something is playing, it returns false and does nothing. In that case the player was already playing so this
+ // track goes to the queue instead.
+ if (!player.startTrack(track, true)) {
+ queue.offer(track);
+ }
+ }
+
+ /**
+ * Start the next track, stopping the current one if it is playing.
+ */
+ public void nextTrack() {
+ // Start the next track, regardless of if something is already playing or not. In case queue was empty, we are
+ // giving null to startTrack, which is a valid argument and will simply stop the player.
+ player.startTrack(queue.poll(), false);
+ }
+
+ @Override
+ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
+ // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED)
+ if (endReason.mayStartNext) {
+ nextTrack();
+ }
+ }
+}