From 4f97ce6e8768c0dba3e21b2e22425a07825408b0 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Tue, 20 Jun 2017 15:18:37 +0200 Subject: [PATCH] Stabilized playback, added playlist class, among other improvements. --- src/main/java/handiebot/HandieBot.java | 2 +- .../handiebot/command/CommandHandler.java | 16 +- .../handiebot/lavaplayer/MusicPlayer.java | 40 ++-- .../java/handiebot/lavaplayer/Playlist.java | 189 ++++++++++++++++++ .../handiebot/lavaplayer/TrackScheduler.java | 50 +++-- 5 files changed, 265 insertions(+), 32 deletions(-) create mode 100644 src/main/java/handiebot/lavaplayer/Playlist.java diff --git a/src/main/java/handiebot/HandieBot.java b/src/main/java/handiebot/HandieBot.java index e8f01f3..93a3210 100644 --- a/src/main/java/handiebot/HandieBot.java +++ b/src/main/java/handiebot/HandieBot.java @@ -21,7 +21,7 @@ public class HandieBot { private static final String TOKEN = "MjgzNjUyOTg5MjEyNjg4Mzg0.C45A_Q.506b0G6my1FEFa7_YY39lxLBHUY"; - private static IDiscordClient client; + public static IDiscordClient client; private CommandHandler commandHandler; private MusicPlayer musicPlayer; diff --git a/src/main/java/handiebot/command/CommandHandler.java b/src/main/java/handiebot/command/CommandHandler.java index d65643e..88d9a9d 100644 --- a/src/main/java/handiebot/command/CommandHandler.java +++ b/src/main/java/handiebot/command/CommandHandler.java @@ -35,10 +35,14 @@ 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){ + DisappearingMessage.deleteMessageAfter(1000, message); + if (command.equals("play")){ //Play or queue a song. - this.bot.getMusicPlayer().loadToQueue(guild, args[0]); + if (args.length == 1) { + this.bot.getMusicPlayer().loadToQueue(guild, args[0]); + } else if (args.length == 0){ + this.bot.getMusicPlayer().playQueue(guild); + } } else if (command.equals("skip") && args.length == 0){ //Skip the current song. this.bot.getMusicPlayer().skipTrack(guild); @@ -53,6 +57,12 @@ public class CommandHandler { //TODO implement repeat command. } else if (command.equals("clear")){ //TODO clear command. + } else if (command.equals("quit")){ + //Quit the application. + channel.sendMessage("Quitting HandieBot functions."); + this.bot.getMusicPlayer().quit(guild); + } else if (command.equals("playlist")){ + //Do playlist actions. } } } diff --git a/src/main/java/handiebot/lavaplayer/MusicPlayer.java b/src/main/java/handiebot/lavaplayer/MusicPlayer.java index 088244e..04071d1 100644 --- a/src/main/java/handiebot/lavaplayer/MusicPlayer.java +++ b/src/main/java/handiebot/lavaplayer/MusicPlayer.java @@ -100,12 +100,7 @@ public class MusicPlayer { 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; + return this.voiceChannels.get(guild); } /** @@ -136,7 +131,8 @@ public class MusicPlayer { } 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()); + IMessage message = getChatChannel(guild).sendMessage(builder.build()); + DisappearingMessage.deleteMessageAfter(6000, message); } } @@ -182,11 +178,8 @@ public class MusicPlayer { public void addToQueue(IGuild guild, AudioTrack track){ IVoiceChannel voiceChannel = getVoiceChannel(guild); if (voiceChannel != null){ - //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; + if (!voiceChannel.isConnected()) { + voiceChannel.join(); } long timeUntilPlay = getMusicManager(guild).scheduler.getTimeUntilDone(); getMusicManager(guild).scheduler.queue(track); @@ -206,6 +199,17 @@ public class MusicPlayer { } + /** + * If possible, try to begin playing from the track scheduler's queue. + */ + public void playQueue(IGuild guild){ + IVoiceChannel vc = this.getVoiceChannel(guild); + if (!vc.isConnected()){ + vc.join(); + } + getMusicManager(guild).scheduler.nextTrack(); + } + /** * Skips the current track. */ @@ -214,4 +218,16 @@ public class MusicPlayer { new DisappearingMessage(getChatChannel(guild), "Skipping the current track.", 3000); } + /** + * Stops playback and disconnects from the voice channel, to cease music actions. + * @param guild The guild to quit from. + */ + public void quit(IGuild guild){ + getMusicManager(guild).scheduler.quit(); + IVoiceChannel vc = this.getVoiceChannel(guild); + if (vc.isConnected()){ + vc.leave(); + } + } + } diff --git a/src/main/java/handiebot/lavaplayer/Playlist.java b/src/main/java/handiebot/lavaplayer/Playlist.java new file mode 100644 index 0000000..abb6c08 --- /dev/null +++ b/src/main/java/handiebot/lavaplayer/Playlist.java @@ -0,0 +1,189 @@ +package handiebot.lavaplayer; + +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.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * @author Andrew Lalis + * A Playlist is a list of AudioTracks which a track scheduler can pull from to create a queue filled with songs. The + * playlist is persistent, i.e. it is saved into a file. + */ +public class Playlist { + + private String name; + private long creatorUID; + + List tracks; + + /** + * Creates an empty playlist template. + * @param name The name of the playlist. + * @param creatorUID The ID of the user who created it. + */ + public Playlist(String name, long creatorUID){ + this.name = name; + this.creatorUID = creatorUID; + this.tracks = new ArrayList<>(); + } + + /** + * Creates a playlist from a file with the given name. + * @param name The name of the file. + */ + public Playlist(String name){ + this.name = name; + this.load(); + } + + public String getName(){ + return this.name; + } + + public long getCreatorUID(){ + return this.creatorUID; + } + + public List getTracks(){ + return this.tracks; + } + + /** + * Adds a track to the end of the playlist. + * @param track The track to add. + */ + public void addTrack(AudioTrack track){ + this.tracks.add(track); + } + + /** + * Removes a track from the playlist. + * @param track The track to remove. + */ + public void removeTrack(AudioTrack track){ + this.tracks.remove(track); + } + + /** + * Returns the next track, i.e. the first one in the list, and removes it from the internal list. + * @return The AudioTrack that should be played next. + */ + public AudioTrack getNextTrackAndRemove(boolean shouldShuffle){ + return this.tracks.remove((shouldShuffle ? getShuffledIndex(this.tracks.size()) : 0)); + } + + /** + * Returns the next track to be played, and re-adds it to the end of the playlist, as it would do in a loop. + * @return + */ + public AudioTrack getNextTrackAndRequeue(boolean shouldShuffle){ + AudioTrack track = this.tracks.remove((shouldShuffle ? getShuffledIndex(this.tracks.size()) : 0)); + this.tracks.add(track); + return track; + } + + /** + * Gets a 'shuffled index' from a given list length. That means: + * - A random number from 0 to (listLength-1) - threshold*(listLength), where threshold is some percentage of + * recent songs that should be ignored; for example, the most recent 20% of the playlist can be ignored. + * - A greater likelihood for numbers closer to 0 (those which have not been played in a while). + * @param listLength The number of items in a potential list to choose from. + * @return A pseudo-random choice as to which item to pick from the list. + */ + public static int getShuffledIndex(int listLength){ + float threshold = 0.2f; + int trueLength = listLength - (int)threshold*listLength; + Random rand = new Random(); + //TODO Add in a small gradient in chance for a song to be picked. + return rand.nextInt(trueLength); + } + + /** + * Saves the playlist to a file in its name. The playlists are saved into a file in the user's home directory. + */ + public void save(){ + String homeDir = System.getProperty("user.home"); + File playlistDir = new File(homeDir+"/.handiebot/playlist"); + if (!playlistDir.exists()){ + if (!playlistDir.mkdirs()){ + System.out.println("Unable to make directory: "+playlistDir.getPath()); + return; + } + } + File playlistFile = new File(playlistDir.getPath()+"/"+this.name.replace(" ", "_")+".txt"); + System.out.println("Saving playlist to: "+playlistFile.getAbsolutePath()); + try(Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(playlistFile)))){ + writer.write(this.name+'\n'); + writer.write(Long.toString(this.creatorUID)+'\n'); + writer.write(Integer.toString(this.tracks.size())+'\n'); + for (AudioTrack track : this.tracks){ + writer.write(track.getInfo().uri); + writer.write('\n'); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Loads the playlist from a file with the playlist's name. + */ + public void load(){//TODO Make load work!!! + String path = System.getProperty("user.home")+"/.handiebot/playlist/"+this.name.replace(" ", "_")+".txt"; + File playlistFile = new File(path); + if (playlistFile.exists()){ + try { + List lines = Files.readAllLines(Paths.get(playlistFile.toURI())); + this.name = lines.remove(0); + this.creatorUID = Long.parseLong(lines.remove(0)); + int trackCount = Integer.parseInt(lines.remove(0)); + this.tracks = new ArrayList<>(trackCount); + AudioPlayerManager pm = new DefaultAudioPlayerManager(); + for (int i = 0; i < trackCount; i++){ + System.out.println("Loading item "+i); + String url = lines.remove(0); + pm.loadItem(url, new AudioLoadResultHandler() { + @Override + public void trackLoaded(AudioTrack audioTrack) { + System.out.println("Added track"); + tracks.add(audioTrack); + } + + @Override + public void playlistLoaded(AudioPlaylist audioPlaylist) { + System.out.println("Playlist loaded."); + //Do nothing. This should not happen. + } + + @Override + public void noMatches() { + System.out.println("No matches for: "+url); + //Do nothing. This should not happen. + } + + @Override + public void loadFailed(FriendlyException e) { + System.out.println("Load failed: "+e.getMessage()); + //Do nothing. This should not happen. + } + }); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + +} diff --git a/src/main/java/handiebot/lavaplayer/TrackScheduler.java b/src/main/java/handiebot/lavaplayer/TrackScheduler.java index 863d43c..1fb86e0 100644 --- a/src/main/java/handiebot/lavaplayer/TrackScheduler.java +++ b/src/main/java/handiebot/lavaplayer/TrackScheduler.java @@ -8,10 +8,7 @@ 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; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; /** * @author Andrew Lalis @@ -19,9 +16,11 @@ import java.util.concurrent.LinkedBlockingQueue; public class TrackScheduler extends AudioEventAdapter { private final AudioPlayer player; - private final BlockingQueue queue; + + private Playlist activePlaylist; private boolean repeat = false; + private boolean shuffle = false; private IGuild guild; @@ -32,7 +31,8 @@ public class TrackScheduler extends AudioEventAdapter { public TrackScheduler(AudioPlayer player, IGuild guild){ this.player = player; this.guild = guild; - this.queue = new LinkedBlockingQueue<>(); + this.activePlaylist = new Playlist("HandieBot Active Playlist", 283652989212688384L); + //this.activePlaylist = new Playlist("HandieBot Active Playlist"); } /** @@ -51,6 +51,22 @@ public class TrackScheduler extends AudioEventAdapter { return this.repeat; } + /** + * Sets whether or not to randomize the next track to be played. + * @param value True if shuffled should become active. + */ + public void setShuffle(boolean value){ + this.shuffle = value; + } + + /** + * Returns whether or not shuffling is active. + * @return True if shuffling is active, false otherwise. + */ + public boolean isShuffling(){ + return this.shuffle; + } + /** * Returns the time until the bot is done playing sound, at the current rate. * @return The milliseconds until music stops. @@ -72,7 +88,7 @@ public class TrackScheduler extends AudioEventAdapter { * @return A list of tracks in the queue. */ public List queueList(){ - return new ArrayList<>(this.queue); + return this.activePlaylist.getTracks(); } /** @@ -83,7 +99,8 @@ public class TrackScheduler extends AudioEventAdapter { if (player.getPlayingTrack() == null){ player.startTrack(track, false); } else { - queue.offer(track); + this.activePlaylist.addTrack(track); + this.activePlaylist.save(); } } @@ -91,11 +108,16 @@ public class TrackScheduler extends AudioEventAdapter { * Starts the next track, stopping the current one if it's playing. */ public void nextTrack(){ - AudioTrack track = queue.poll(); + AudioTrack track = (this.repeat ? this.activePlaylist.getNextTrackAndRequeue(this.shuffle) : this.activePlaylist.getNextTrackAndRemove(this.shuffle)); + this.activePlaylist.save(); player.startTrack(track, false); - if (this.repeat){ - this.queue.add(track); - } + } + + /** + * If the user wishes to quit, stop the currently played track. + */ + public void quit(){ + this.player.stopTrack(); } @Override @@ -111,6 +133,7 @@ public class TrackScheduler extends AudioEventAdapter { public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) { System.out.println("Track ended."); if (endReason.mayStartNext){ + System.out.println("Moving to next track."); nextTrack(); } else { System.out.println(endReason.toString()); @@ -122,9 +145,4 @@ public class TrackScheduler extends AudioEventAdapter { exception.printStackTrace(); } - @Override - public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) { - super.onTrackStuck(player, track, thresholdMs); - } - }