diff --git a/src/main/java/handiebot/HandieBot.java b/src/main/java/handiebot/HandieBot.java index ae486ec..e8f01f3 100644 --- a/src/main/java/handiebot/HandieBot.java +++ b/src/main/java/handiebot/HandieBot.java @@ -1,10 +1,6 @@ package handiebot; -import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; import handiebot.command.CommandHandler; -import handiebot.lavaplayer.GuildMusicManager; import handiebot.lavaplayer.MusicPlayer; import sx.blah.discord.api.ClientBuilder; import sx.blah.discord.api.IDiscordClient; @@ -13,8 +9,7 @@ import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedE import sx.blah.discord.util.DiscordException; import sx.blah.discord.util.RateLimitException; -import java.util.HashMap; -import java.util.Map; +import java.util.logging.Logger; /** * @author Andrew Lalis @@ -22,29 +17,16 @@ import java.util.Map; */ public class HandieBot { + public static Logger log = Logger.getLogger("HandieBotLog"); + private static final String TOKEN = "MjgzNjUyOTg5MjEyNjg4Mzg0.C45A_Q.506b0G6my1FEFa7_YY39lxLBHUY"; private static IDiscordClient client; private CommandHandler commandHandler; - - public static void main(String[] args) throws DiscordException, RateLimitException { - System.out.println("Logging bot in..."); - client = new ClientBuilder().withToken(TOKEN).build(); - client.getDispatcher().registerListener(new HandieBot()); - client.login(); - } - - private final AudioPlayerManager playerManager; - private final Map musicManagers; private MusicPlayer musicPlayer; private HandieBot() { - this.musicManagers = new HashMap<>(); - this.playerManager = new DefaultAudioPlayerManager(); - AudioSourceManagers.registerRemoteSources(playerManager); - AudioSourceManagers.registerLocalSource(playerManager); - this.commandHandler = new CommandHandler(this); this.musicPlayer = new MusicPlayer(); } @@ -58,5 +40,11 @@ public class HandieBot { this.commandHandler.handleCommand(event); } + public static void main(String[] args) throws DiscordException, RateLimitException { + System.out.println("Logging bot in."); + client = new ClientBuilder().withToken(TOKEN).build(); + client.getDispatcher().registerListener(new HandieBot()); + client.login(); + } } diff --git a/src/main/java/handiebot/command/CommandHandler.java b/src/main/java/handiebot/command/CommandHandler.java index cae19a9..d65643e 100644 --- a/src/main/java/handiebot/command/CommandHandler.java +++ b/src/main/java/handiebot/command/CommandHandler.java @@ -2,6 +2,7 @@ package handiebot.command; import com.sun.istack.internal.NotNull; import handiebot.HandieBot; +import handiebot.utils.DisappearingMessage; import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; import sx.blah.discord.handle.obj.*; import sx.blah.discord.util.EmbedBuilder; @@ -34,16 +35,24 @@ public class CommandHandler { String command = extractCommand(message); String[] args = extractArgs(message); if (guild != null && command != null){ + DisappearingMessage.deleteMessageAfter(2000, message); if (command.equals("play") && args.length == 1){ + //Play or queue a song. this.bot.getMusicPlayer().loadToQueue(guild, args[0]); } else if (command.equals("skip") && args.length == 0){ + //Skip the current song. this.bot.getMusicPlayer().skipTrack(guild); } else if (command.equals("help")){ - this.sendHelpInfo(user); + //Send a PM to the user with help info. + this.sendHelpInfo(user);//TODO finish the help command and fill in with new descriptions each time. } else if (command.equals("queue") && args.length == 0){ + //Display the first few items of the queue. this.bot.getMusicPlayer().showQueueList(guild); } else if (command.equals("repeat")){ - this.bot.getMusicPlayer().toggleRepeat(guild); + //Toggle repeat. + //TODO implement repeat command. + } else if (command.equals("clear")){ + //TODO clear command. } } } diff --git a/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java b/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java deleted file mode 100644 index a07d21d..0000000 --- a/src/main/java/handiebot/lavaplayer/AudioLoadResultHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package handiebot.lavaplayer; - -import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; -import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; -import com.sedmelluq.discord.lavaplayer.track.AudioTrack; - -/** - * @author Andrew Lalis - */ -public class AudioLoadResultHandler implements com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler { - - private TrackScheduler scheduler; - - public AudioLoadResultHandler(TrackScheduler scheduler){ - this.scheduler = scheduler; - } - - @Override - public void trackLoaded(AudioTrack audioTrack) { - System.out.println("Adding to queue "+ audioTrack.getInfo().title); - scheduler.queue(audioTrack); - } - - @Override - public void playlistLoaded(AudioPlaylist audioPlaylist) { - System.out.println("Adding playlist to queue."); - audioPlaylist.getTracks().forEach(track -> this.scheduler.queue(track)); - } - - @Override - public void noMatches() { - System.out.println("No matches!"); - } - - @Override - public void loadFailed(FriendlyException e) { - System.out.println("Load failed."); - e.printStackTrace(); - } -} diff --git a/src/main/java/handiebot/lavaplayer/GuildMusicManager.java b/src/main/java/handiebot/lavaplayer/GuildMusicManager.java index 06af150..54f8268 100644 --- a/src/main/java/handiebot/lavaplayer/GuildMusicManager.java +++ b/src/main/java/handiebot/lavaplayer/GuildMusicManager.java @@ -2,6 +2,7 @@ package handiebot.lavaplayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import sx.blah.discord.handle.obj.IGuild; /** * @author Andrew Lalis @@ -13,9 +14,9 @@ public class GuildMusicManager { public final TrackScheduler scheduler; - public GuildMusicManager(AudioPlayerManager manager){ + public GuildMusicManager(AudioPlayerManager manager, IGuild guild){ this.player = manager.createPlayer(); - this.scheduler = new TrackScheduler(this.player); + this.scheduler = new TrackScheduler(this.player, guild); this.player.addListener(this.scheduler); } diff --git a/src/main/java/handiebot/lavaplayer/MusicPlayer.java b/src/main/java/handiebot/lavaplayer/MusicPlayer.java index 039da59..088244e 100644 --- a/src/main/java/handiebot/lavaplayer/MusicPlayer.java +++ b/src/main/java/handiebot/lavaplayer/MusicPlayer.java @@ -8,14 +8,17 @@ import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import handiebot.command.CommandHandler; +import handiebot.utils.DisappearingMessage; import sx.blah.discord.handle.obj.IChannel; import sx.blah.discord.handle.obj.IGuild; +import sx.blah.discord.handle.obj.IMessage; import sx.blah.discord.handle.obj.IVoiceChannel; import sx.blah.discord.util.EmbedBuilder; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * @author Andrew Lalis @@ -24,37 +27,94 @@ import java.util.Map; */ public class MusicPlayer { - private static String CHANNEL_NAME = "music"; + //Name for the message and voice channels dedicated to this bot. + public static String CHANNEL_NAME = "HandieBotMusic"; private final AudioPlayerManager playerManager; - private final Map musicManagers; + + /* + Mappings of music managers, channels and voice channels for each guild. + */ + private Map musicManagers; + private Map chatChannels; + private Map voiceChannels; public MusicPlayer(){ - this.musicManagers = new HashMap<>(); + //Initialize player manager. this.playerManager = new DefaultAudioPlayerManager(); AudioSourceManagers.registerLocalSource(playerManager); AudioSourceManagers.registerRemoteSources(playerManager); + + //Initialize all maps. + this.musicManagers = new HashMap<>(); + this.chatChannels = new HashMap<>(); + this.voiceChannels = new HashMap<>(); } /** - * Toggles the playlist's repeating. - * @param guild The guild to perform the action on. + * Gets the music manager specific to a particular guild. + * @param guild The guild to get the music manager for. + * @return The music manager for a guild. */ - public void toggleRepeat(IGuild guild){ - GuildMusicManager musicManager = this.getGuildMusicManager(guild); - musicManager.scheduler.setRepeat(!musicManager.scheduler.isRepeating()); - this.getMessageChannel(guild).sendMessage("**Repeat** is now *"+(musicManager.scheduler.isRepeating() ? "On" : "Off")+"*."); + private GuildMusicManager getMusicManager(IGuild guild){ + if (!this.musicManagers.containsKey(guild)){ + System.out.println("Registering guild, creating audio provider."); + this.musicManagers.put(guild, new GuildMusicManager(this.playerManager, guild)); + guild.getAudioManager().setAudioProvider(this.musicManagers.get(guild).getAudioProvider()); + } + return this.musicManagers.get(guild); + } + + /** + * Gets the chat channel specific to a particular guild. This channel is used send updates about playback and + * responses to people's commands. If none exists, the bot will attempt to make a channel. + * @param guild The guild to get the channel from. + * @return A message channel on a particular guild that is specifically for music. + */ + private IChannel getChatChannel(IGuild guild){ + if (!this.chatChannels.containsKey(guild)){ + List channels = guild.getChannelsByName(CHANNEL_NAME.toLowerCase()); + if (channels.isEmpty()){ + System.out.println("Found "+channels.size()+" matches for message channel, creating new one."); + this.chatChannels.put(guild, guild.createChannel(CHANNEL_NAME.toLowerCase())); + } else { + this.chatChannels.put(guild, channels.get(0)); + } + } + return this.chatChannels.get(guild); + } + + /** + * Gets the voice channel associated with a particular guild. This channel is used for audio playback. If none + * exists, the bot will attempt to make a voice channel. + * @param guild The guild to get the channel from. + * @return The voice channel on a guild that is for this bot. + */ + private IVoiceChannel getVoiceChannel(IGuild guild){ + if (!this.voiceChannels.containsKey(guild)){ + List channels = guild.getVoiceChannelsByName(CHANNEL_NAME); + if (channels.isEmpty()){ + System.out.println("Found "+channels.size()+" matches for voice channel, creating new one."); + this.voiceChannels.put(guild, guild.createVoiceChannel(CHANNEL_NAME)); + } else { + this.voiceChannels.put(guild, channels.get(0)); + } + } + IVoiceChannel vc = this.voiceChannels.get(guild); + if (!vc.isConnected()){ + System.out.println("Joined voice channel."); + vc.join(); + } + return vc; } /** * Sends a formatted message to the guild about the first few items in a queue. - * @param guild The guild to show the queue for. */ public void showQueueList(IGuild guild){ - GuildMusicManager musicManager = this.getGuildMusicManager(guild); - List tracks = musicManager.scheduler.queueList(); + List tracks = getMusicManager(guild).scheduler.queueList(); if (tracks.size() == 0) { - this.getMessageChannel(guild).sendMessage("The queue is empty. Use **"+ CommandHandler.PREFIX+"play** *URL* to add songs."); + new DisappearingMessage(getChatChannel(guild), "The queue is empty. Use **"+ CommandHandler.PREFIX+"play** *URL* to add songs.", 3000); } else { EmbedBuilder builder = new EmbedBuilder(); builder.withColor(255, 0, 0); @@ -74,22 +134,22 @@ public class MusicPlayer { sb.append(seconds % 60); sb.append("]\n"); } - builder.appendField("Showing " + (tracks.size() <= 10 ? tracks.size() : "the first 10") + " tracks.", sb.toString(), false); - this.getMessageChannel(guild).sendMessage(builder.build()); + builder.withTimestamp(System.currentTimeMillis()); + builder.appendField("Showing " + (tracks.size() <= 10 ? tracks.size() : "the first 10") + " track"+(tracks.size() > 1 ? "s" : "")+".", sb.toString(), false); + getChatChannel(guild).sendMessage(builder.build()); } } /** * Loads a URL to the queue, or outputs an error message if it fails. - * @param guild The guild to load the URL to. * @param trackURL A string representing a youtube/soundcloud URL. */ public void loadToQueue(IGuild guild, String trackURL){ - GuildMusicManager musicManager = this.getGuildMusicManager(guild); - this.playerManager.loadItemOrdered(musicManager, trackURL, new AudioLoadResultHandler() { + this.playerManager.loadItemOrdered(getMusicManager(guild), trackURL, new AudioLoadResultHandler() { @Override public void trackLoaded(AudioTrack audioTrack) { - addToQueue(guild, musicManager, audioTrack); + System.out.println("Track successfully loaded: "+audioTrack.getInfo().title); + addToQueue(guild, audioTrack); } @Override @@ -99,92 +159,59 @@ public class MusicPlayer { if (firstTrack == null){ firstTrack = audioPlaylist.getTracks().get(0); } - addToQueue(guild, musicManager,firstTrack); + addToQueue(guild, firstTrack); } } @Override public void noMatches() { - getMessageChannel(guild).sendMessage("Unable to find a result for: "+trackURL); + new DisappearingMessage(getChatChannel(guild), "Unable to find a result for: "+trackURL, 3000); } @Override public void loadFailed(FriendlyException e) { - getMessageChannel(guild).sendMessage("Unable to load. "+e.getMessage()); + new DisappearingMessage(getChatChannel(guild), "Unable to load. "+e.getMessage(), 3000); } }); } /** * Adds a track to the queue and sends a message to the appropriate channel notifying users. - * @param guild The guild to queue the track in. - * @param musicManager The music manager to use. * @param track The track to queue. */ - public void addToQueue(IGuild guild, GuildMusicManager musicManager, AudioTrack track){ - IVoiceChannel voiceChannel = this.connectToMusicChannel(guild); + public void addToQueue(IGuild guild, AudioTrack track){ + IVoiceChannel voiceChannel = getVoiceChannel(guild); if (voiceChannel != null){ - musicManager.scheduler.queue(track); - IChannel channel = this.getMessageChannel(guild); - channel.sendMessage("Added **"+track.getInfo().title+"** to the queue."); + //Check to make sure sound can be played. + if (guild.getAudioManager().getAudioProvider() == null){ + new DisappearingMessage(getChatChannel(guild), "Audio provider not set. Please try again.", 3000); + guild.getAudioManager().setAudioProvider(getMusicManager(guild).getAudioProvider()); + return; + } + long timeUntilPlay = getMusicManager(guild).scheduler.getTimeUntilDone(); + getMusicManager(guild).scheduler.queue(track); + //Build message. + StringBuilder sb = new StringBuilder(); + sb.append("Added **").append(track.getInfo().title).append("** to the queue."); + //If there's some tracks in the queue, get the time until this one plays. + if (timeUntilPlay != 0){ + sb.append(String.format("\nTime until play: %d min, %d sec", + TimeUnit.MILLISECONDS.toMinutes(timeUntilPlay), + TimeUnit.MILLISECONDS.toSeconds(timeUntilPlay) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeUntilPlay)) + )); + } + IMessage message = getChatChannel(guild).sendMessage(sb.toString()); + DisappearingMessage.deleteMessageAfter(3000, message); } } /** * Skips the current track. - * @param guild The guild to perform the skip on. */ public void skipTrack(IGuild guild){ - this.getGuildMusicManager(guild).scheduler.nextTrack(); - this.getMessageChannel(guild).sendMessage("Skipping the current track."); - } - - /** - * Gets or creates a music manager for a specific guild. - * @param guild The guild to get a manager for. - * @return A Music Manager for the guild. - */ - private synchronized GuildMusicManager getGuildMusicManager(IGuild guild){ - long guildId = Long.parseLong(guild.getStringID()); - GuildMusicManager musicManager = this.musicManagers.get(guildId); - if (musicManager == null){ - musicManager = new GuildMusicManager(this.playerManager); - musicManagers.put(guildId, musicManager); - } - guild.getAudioManager().setAudioProvider(musicManager.getAudioProvider()); - return musicManager; - } - - /** - * Searches for and attempts to connect to a channel called 'music'. - * @param guild the guild to get voice channels from. - * @return The voice channel the bot is now connected to. - */ - private IVoiceChannel connectToMusicChannel(IGuild guild){ - List voiceChannels = guild.getVoiceChannelsByName(CHANNEL_NAME); - if (voiceChannels.size() == 1){ - if (!voiceChannels.get(0).isConnected()) - voiceChannels.get(0).join(); - return voiceChannels.get(0); - } - IVoiceChannel voiceChannel = guild.createVoiceChannel(CHANNEL_NAME); - voiceChannel.join(); - return voiceChannel; - } - - /** - * Returns a 'music' message channel where the bot can post info on playing songs, user requests, - * etc. - * @param guild The guild to get channels from. - * @return The channel with that name. - */ - private IChannel getMessageChannel(IGuild guild){ - List channels = guild.getChannelsByName(CHANNEL_NAME); - if (channels.size() == 1){ - return channels.get(0); - } - return guild.createChannel(CHANNEL_NAME); + getMusicManager(guild).scheduler.nextTrack(); + new DisappearingMessage(getChatChannel(guild), "Skipping the current track.", 3000); } } diff --git a/src/main/java/handiebot/lavaplayer/TrackScheduler.java b/src/main/java/handiebot/lavaplayer/TrackScheduler.java index 18fff1a..863d43c 100644 --- a/src/main/java/handiebot/lavaplayer/TrackScheduler.java +++ b/src/main/java/handiebot/lavaplayer/TrackScheduler.java @@ -5,6 +5,8 @@ import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IGuild; import java.util.ArrayList; import java.util.List; @@ -18,15 +20,18 @@ public class TrackScheduler extends AudioEventAdapter { private final AudioPlayer player; private final BlockingQueue queue; + private boolean repeat = false; - private AudioTrack currentTrack = null; + + private IGuild guild; /** * Constructs a new track scheduler with the given player. * @param player The audio player this scheduler uses. */ - public TrackScheduler(AudioPlayer player){ + public TrackScheduler(AudioPlayer player, IGuild guild){ this.player = player; + this.guild = guild; this.queue = new LinkedBlockingQueue<>(); } @@ -46,13 +51,38 @@ public class TrackScheduler extends AudioEventAdapter { return this.repeat; } + /** + * Returns the time until the bot is done playing sound, at the current rate. + * @return The milliseconds until music stops. + */ + public long getTimeUntilDone(){ + long t = 0; + AudioTrack currentTrack = this.player.getPlayingTrack(); + if (currentTrack != null){ + t += currentTrack.getDuration() - currentTrack.getPosition(); + } + for (AudioTrack track : this.queueList()){ + t += track.getDuration(); + } + return t; + } + + /** + * Returns a list of tracks in the queue. + * @return A list of tracks in the queue. + */ + public List queueList(){ + return new ArrayList<>(this.queue); + } + /** * Add the next track to the queue or play right away if nothing is in the queue. * @param track The track to play or add to the queue. */ public void queue(AudioTrack track){ - if (!player.startTrack(track, true)){ - System.out.println("Unable to start track immediately, adding to queue."); + if (player.getPlayingTrack() == null){ + player.startTrack(track, false); + } else { queue.offer(track); } } @@ -63,28 +93,28 @@ public class TrackScheduler extends AudioEventAdapter { public void nextTrack(){ AudioTrack track = queue.poll(); player.startTrack(track, false); - this.currentTrack = track; if (this.repeat){ this.queue.add(track); } } @Override - public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { - if (endReason.mayStartNext){ - nextTrack(); - } else { - this.currentTrack = null; - System.out.println(endReason.toString()); + public void onTrackStart(AudioPlayer player, AudioTrack track) { + System.out.println("Started audio track: "+track.getInfo().title); + List channels = this.guild.getChannelsByName(MusicPlayer.CHANNEL_NAME.toLowerCase()); + if (channels.size() > 0){ + channels.get(0).sendMessage("Now playing: **"+track.getInfo().title+"**."); } } - /** - * Returns a list of tracks in the queue. - * @return A list of tracks in the queue. - */ - public List queueList(){ - return new ArrayList<>(this.queue); + @Override + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + System.out.println("Track ended."); + if (endReason.mayStartNext){ + nextTrack(); + } else { + System.out.println(endReason.toString()); + } } @Override @@ -96,4 +126,5 @@ public class TrackScheduler extends AudioEventAdapter { public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { super.onTrackStuck(player, track, thresholdMs); } + } diff --git a/src/main/java/handiebot/utils/DisappearingMessage.java b/src/main/java/handiebot/utils/DisappearingMessage.java new file mode 100644 index 0000000..caf5382 --- /dev/null +++ b/src/main/java/handiebot/utils/DisappearingMessage.java @@ -0,0 +1,44 @@ +package handiebot.utils; + +import sx.blah.discord.handle.obj.IChannel; +import sx.blah.discord.handle.obj.IMessage; + +/** + * @author Andrew Lalis + * Creates a message on a channel that will disappear after some time. + */ +public class DisappearingMessage extends Thread implements Runnable { + + /** + * Creates a new disappearing message that times out after some time. + * @param channel The channel to write the message in. + * @param message The message content. + * @param timeout How long until the message is deleted. + */ + public DisappearingMessage(IChannel channel, String message, long timeout){ + IMessage sentMessage = channel.sendMessage(message); + try { + sleep(timeout); + } catch (InterruptedException e) { + e.printStackTrace(); + } + sentMessage.delete(); + } + + /** + * Deletes a message after a set amount of time. + * @param timeout The delay until deletion, in milliseconds. + * @param message The message to delete. + */ + public static void deleteMessageAfter(long timeout, IMessage message){ + new Thread(() -> { + try { + sleep(timeout); + message.delete(); + } catch (InterruptedException e){ + e.printStackTrace(); + } + }).start(); + } + +}