From 7eeb97fc307257290acfbf6ecbb055cdc5788c2b Mon Sep 17 00:00:00 2001
From: Brandon <bwaggone@umich.edu>
Date: Sat, 7 Apr 2018 17:09:12 -0400
Subject: Rename main, add properties loading

---
 build.gradle                                       |   2 +-
 src/main/java/soundbot/AudioPlayerSendHandler.java |  48 -----
 src/main/java/soundbot/GuildMusicManager.java      |  35 ----
 src/main/java/soundbot/Main.java                   | 167 -----------------
 src/main/java/soundbot/TrackScheduler.java         |  56 ------
 .../java/soundchan/AudioPlayerSendHandler.java     |  48 +++++
 src/main/java/soundchan/GuildMusicManager.java     |  35 ++++
 src/main/java/soundchan/Main.java                  | 201 +++++++++++++++++++++
 src/main/java/soundchan/TrackScheduler.java        |  56 ++++++
 9 files changed, 341 insertions(+), 307 deletions(-)
 delete mode 100644 src/main/java/soundbot/AudioPlayerSendHandler.java
 delete mode 100644 src/main/java/soundbot/GuildMusicManager.java
 delete mode 100644 src/main/java/soundbot/Main.java
 delete mode 100644 src/main/java/soundbot/TrackScheduler.java
 create mode 100644 src/main/java/soundchan/AudioPlayerSendHandler.java
 create mode 100644 src/main/java/soundchan/GuildMusicManager.java
 create mode 100644 src/main/java/soundchan/Main.java
 create mode 100644 src/main/java/soundchan/TrackScheduler.java

diff --git a/build.gradle b/build.gradle
index cbaf6bd..5ecc1c7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,4 +13,4 @@ dependencies {
   runtime 'ch.qos.logback:logback-classic:1.2.3'
 }
 
-mainClassName = 'soundbot.Main'
+mainClassName = 'soundchan.Main'
diff --git a/src/main/java/soundbot/AudioPlayerSendHandler.java b/src/main/java/soundbot/AudioPlayerSendHandler.java
deleted file mode 100644
index 6499aad..0000000
--- a/src/main/java/soundbot/AudioPlayerSendHandler.java
+++ /dev/null
@@ -1,48 +0,0 @@
-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
deleted file mode 100644
index fc46756..0000000
--- a/src/main/java/soundbot/GuildMusicManager.java
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 2255c68..0000000
--- a/src/main/java/soundbot/Main.java
+++ /dev/null
@@ -1,167 +0,0 @@
-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
deleted file mode 100644
index 4cbda71..0000000
--- a/src/main/java/soundbot/TrackScheduler.java
+++ /dev/null
@@ -1,56 +0,0 @@
-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();
-    }
-  }
-}
diff --git a/src/main/java/soundchan/AudioPlayerSendHandler.java b/src/main/java/soundchan/AudioPlayerSendHandler.java
new file mode 100644
index 0000000..cd76807
--- /dev/null
+++ b/src/main/java/soundchan/AudioPlayerSendHandler.java
@@ -0,0 +1,48 @@
+package soundchan;
+
+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/soundchan/GuildMusicManager.java b/src/main/java/soundchan/GuildMusicManager.java
new file mode 100644
index 0000000..40a0258
--- /dev/null
+++ b/src/main/java/soundchan/GuildMusicManager.java
@@ -0,0 +1,35 @@
+package soundchan;
+
+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/soundchan/Main.java b/src/main/java/soundchan/Main.java
new file mode 100644
index 0000000..80430ba
--- /dev/null
+++ b/src/main/java/soundchan/Main.java
@@ -0,0 +1,201 @@
+package soundchan;
+
+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.MessageChannel;
+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.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+public class Main extends ListenerAdapter {
+  public static void main(String[] args) throws Exception {
+
+    Properties properties = LoadProperties();
+
+    JDA jda = new JDABuilder(AccountType.BOT)
+        .setToken(properties.getProperty("botToken"))
+        .buildBlocking();
+
+    jda.addEventListener(new Main());
+  }
+
+  private static Properties LoadProperties(){
+    Properties properties = new Properties();
+    InputStream input = null;
+      try{
+        input = new FileInputStream("soundchan.properties");
+        properties.load(input);
+
+      }catch (IOException ex){
+        ex.printStackTrace();
+      } finally {
+        try {
+          input.close();
+        } catch (IOException ex) {
+          ex.printStackTrace();
+        }
+      }
+    return properties;
+  }
+
+  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();
+    MessageChannel channel = null;
+
+    // This means SoundChan was DM'd
+    if (guild == null){
+      channel = event.getPrivateChannel();
+    }else{
+      channel = event.getTextChannel();
+    }
+
+    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/soundchan/TrackScheduler.java b/src/main/java/soundchan/TrackScheduler.java
new file mode 100644
index 0000000..7f1c258
--- /dev/null
+++ b/src/main/java/soundchan/TrackScheduler.java
@@ -0,0 +1,56 @@
+package soundchan;
+
+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();
+    }
+  }
+}
-- 
cgit v1.2.3