Version 1: Basic functionality #1

Merged
andrewlalis merged 10 commits from development into master 2017-06-21 14:48:08 +00:00
7 changed files with 221 additions and 161 deletions
Showing only changes of commit 0ceedf293d - Show all commits

View File

@ -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<Long, GuildMusicManager> 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();
}
}

View File

@ -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.
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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<Long, GuildMusicManager> musicManagers;
/*
Mappings of music managers, channels and voice channels for each guild.
*/
private Map<IGuild, GuildMusicManager> musicManagers;
private Map<IGuild, IChannel> chatChannels;
private Map<IGuild, IVoiceChannel> 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<IChannel> 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<IVoiceChannel> 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<AudioTrack> tracks = musicManager.scheduler.queueList();
List<AudioTrack> 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<IVoiceChannel> 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<IChannel> 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);
}
}

View File

@ -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<AudioTrack> 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<AudioTrack> 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<IChannel> 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<AudioTrack> 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);
}
}

View File

@ -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();
}
}