diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a8565d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +**/target/ \ No newline at end of file diff --git a/README.md b/README.md index eb830d0..ecc5e9b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Siren +A Docker containerized version of [MusicBot](https://github.com/jagrosh/MusicBot) `docker build .` -`docker-compose up -d` +`docker-compose up -d` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..88c2f64 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + com.bensherriff + Siren + Snapshot + jar + + + + central + Maven Central + default + https://repo1.maven.org/maven2 + + + dv8tion + m2-dv8tion + https://m2.dv8tion.net/releases + + + + + UTF-8 + 17 + 17 + + + + + + net.dv8tion + JDA + + 4.2.1_253 + + + com.sedmelluq + lavaplayer + 1.3.77 + + + + org.slf4j + slf4j-api + 2.0.5 + + + org.apache.logging.log4j + log4j-api + 2.19.0 + + + org.apache.logging.log4j + log4j-core + 2.19.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.5 + + + package + + shade + + + true + All + + + *:* + + + + + reference.conf + + + + com.bensherriff.siren.MusicBot + ${project.artifactId} + ${project.version} + ${project.artifactId} + ${project.version} + ${project.groupId} + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/bensherriff/siren/AudioPlayerManager.java b/src/main/java/com/bensherriff/siren/AudioPlayerManager.java new file mode 100644 index 0000000..8564774 --- /dev/null +++ b/src/main/java/com/bensherriff/siren/AudioPlayerManager.java @@ -0,0 +1,12 @@ +package com.bensherriff.siren; + +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; + +public class AudioPlayerManager extends DefaultAudioPlayerManager { + + public void initialize() { + AudioSourceManagers.registerRemoteSources(this); + AudioSourceManagers.registerLocalSource(this); + } +} diff --git a/src/main/java/com/bensherriff/siren/AudioPlayerSendHandler.java b/src/main/java/com/bensherriff/siren/AudioPlayerSendHandler.java new file mode 100644 index 0000000..bbda831 --- /dev/null +++ b/src/main/java/com/bensherriff/siren/AudioPlayerSendHandler.java @@ -0,0 +1,39 @@ +package com.bensherriff.siren; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame; +import java.nio.Buffer; +import net.dv8tion.jda.api.audio.AudioSendHandler; + +import java.nio.ByteBuffer; + +public class AudioPlayerSendHandler implements AudioSendHandler { + private final AudioPlayer audioPlayer; + private final ByteBuffer buffer; + private final MutableAudioFrame frame; + + public AudioPlayerSendHandler(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + this.buffer = ByteBuffer.allocate(1024); + this.frame = new MutableAudioFrame(); + this.frame.setBuffer(buffer); + } + + @Override + public boolean canProvide() { + // returns true if audio was provided + return audioPlayer.provide(frame); + } + + @Override + public ByteBuffer provide20MsAudio() { + // flip to make it a read buffer + ((Buffer) buffer).flip(); + return buffer; + } + + @Override + public boolean isOpus() { + return true; + } +} diff --git a/src/main/java/com/bensherriff/siren/MusicBot.java b/src/main/java/com/bensherriff/siren/MusicBot.java new file mode 100644 index 0000000..6a137ea --- /dev/null +++ b/src/main/java/com/bensherriff/siren/MusicBot.java @@ -0,0 +1,132 @@ +package com.bensherriff.siren; + +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.api.JDABuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.managers.AudioManager; + +import java.util.HashMap; +import java.util.Map; + +import static net.dv8tion.jda.api.requests.GatewayIntent.GUILD_MESSAGES; +import static net.dv8tion.jda.api.requests.GatewayIntent.GUILD_VOICE_STATES; + +public class MusicBot extends ListenerAdapter { + public static void main(String[] args) throws Exception { + + JDABuilder.create("OTMyMzAxMjQ4NTQ2MzYxMzQ2.YeQ_Mg.n4H8Cl3dQ1u5aFL1ZvTmfcGwpEY", GUILD_MESSAGES, GUILD_VOICE_STATES) + .addEventListeners(new MusicBot()) + .build(); + } + + private final AudioPlayerManager playerManager; + private final Map musicManagers; + + private MusicBot() { + this.musicManagers = new HashMap<>(); + + this.playerManager = new DefaultAudioPlayerManager(); + AudioSourceManagers.registerRemoteSources(playerManager); + AudioSourceManagers.registerLocalSource(playerManager); + } + + private synchronized MusicManager getGuildAudioPlayer(Guild guild) { + long guildId = Long.parseLong(guild.getId()); + MusicManager musicManager = musicManagers.get(guildId); + + if (musicManager == null) { + musicManager = new MusicManager(playerManager); + musicManagers.put(guildId, musicManager); + } + + guild.getAudioManager().setSendingHandler(musicManager.getSendHandler()); + + return musicManager; + } + + @Override + public void onGuildMessageReceived(GuildMessageReceivedEvent event) { + String[] command = event.getMessage().getContentRaw().split(" ", 2); + if (event.getAuthor().isBot()) return; + + TextChannel channel = event.getChannel(); + + if ("!play".equals(command[0]) && command.length == 2) { + loadAndPlay(channel, command[1]); + } else if ("!skip".equals(command[0])) { + skipTrack(channel); + } else if ("!stop".equals(command[0])) { + + } + + super.onGuildMessageReceived(event); + } + + private void loadAndPlay(final TextChannel channel, final String trackUrl) { + MusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + + playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() { + @Override + public void trackLoaded(AudioTrack track) { + channel.sendMessage("Adding **" + track.getInfo().title + "** to queue").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 **" + firstTrack.getInfo().title + "** to queue (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, MusicManager musicManager, AudioTrack track) { + connectToFirstVoiceChannel(guild.getAudioManager()); + + musicManager.scheduler.queue(track); + } + + private void skipTrack(TextChannel channel) { + MusicManager musicManager = getGuildAudioPlayer(channel.getGuild()); + musicManager.scheduler.nextTrack(); + + channel.sendMessage("Skipped to next track.").queue(); + } + + private static void connectToFirstVoiceChannel(AudioManager audioManager) { + if (!audioManager.isConnected()) { + for (VoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) { + audioManager.openAudioConnection(voiceChannel); + break; + } + } + } +} diff --git a/src/main/java/com/bensherriff/siren/MusicManager.java b/src/main/java/com/bensherriff/siren/MusicManager.java new file mode 100644 index 0000000..80dc631 --- /dev/null +++ b/src/main/java/com/bensherriff/siren/MusicManager.java @@ -0,0 +1,20 @@ +package com.bensherriff.siren; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; + +public class MusicManager { + + public final AudioPlayer player; + public final TrackScheduler scheduler; + + public MusicManager(AudioPlayerManager manager) { + player = manager.createPlayer(); + scheduler = new TrackScheduler(player); + player.addListener(scheduler); + } + + public AudioPlayerSendHandler getSendHandler() { + return new AudioPlayerSendHandler(player); + } +} \ No newline at end of file diff --git a/src/main/java/com/bensherriff/siren/TrackScheduler.java b/src/main/java/com/bensherriff/siren/TrackScheduler.java new file mode 100644 index 0000000..45cd4d0 --- /dev/null +++ b/src/main/java/com/bensherriff/siren/TrackScheduler.java @@ -0,0 +1,56 @@ +package com.bensherriff.siren; + +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 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(); + } + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..ba459e6 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file