Skip to content

Commit

Permalink
Added functionality for Kensa to streaming music and join/leave voice…
Browse files Browse the repository at this point in the history
… channels.

Kensa can now stream and queue music from urls and youtube links.
Pretty basic functionality for now but seems to be working fairly good.

Will add javadoc at a later stage when I am in the mood for that.
  • Loading branch information
marlind89 committed Jul 31, 2016
1 parent 3c2b425 commit f2474ee
Show file tree
Hide file tree
Showing 12 changed files with 661 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
*.iml
.idea

target
34 changes: 34 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,39 @@
<artifactId>kensa</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<repositories>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>com.github.austinv11</groupId>
<artifactId>Discord4j</artifactId>
<version>2.5.2</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</project>
56 changes: 56 additions & 0 deletions src/main/java/com/github/langebangen/kensa/Action.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.langebangen.kensa;

/**
* @author langen
*/
public enum Action
{
HELP ("help", "Shows this help description."),
JOIN ("join", "Joins the specified voice channel."),
LEAVE ("leave", "Leaves the current channel Kensa is in."),
PLAY ("play", "Queues the specified song in the playlist from the specified URL. This play function supports youtube links and urls that ends with .mp3, .ogg, .flac, or .wav."),
SKIP ("skip", "Skips the current song and additional future songs if a number is provided."),
SONG ("song", "Shows the current track."),
PLAYLIST ("playlist", "Shows the playlist."),
CLEAR ("clear", "Clears the playlist.");

private final String action;
private final int argsAmount;
private final String description;

Action(String command, String description)
{
this(command, 0, description);
}

Action(String command, int argsAmount, String description)
{
this.action = "!" + command;
this.argsAmount = argsAmount;
this.description = description;
}

public String getAction()
{
return action;
}

public String getDescription()
{
return description;
}

public static Action getAction(String commandValue)
{
for(Action command : values())
{
if(command.action.equalsIgnoreCase(commandValue))
{
return command;
}
}

return null;
}

}
232 changes: 232 additions & 0 deletions src/main/java/com/github/langebangen/kensa/AudioStreamer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package com.github.langebangen.kensa;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MessageBuilder;
import sx.blah.discord.util.MissingPermissionsException;
import sx.blah.discord.util.RateLimitException;
import sx.blah.discord.util.audio.AudioPlayer;
import sx.blah.discord.util.audio.providers.URLProvider;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**
* @author langen
*/
public class AudioStreamer
{
private static final Logger logger = LoggerFactory.getLogger(AudioStreamer.class);

public static void stream(String urlString, AudioPlayer player, MessageBuilder messageBuilder)
{
if(urlString.contains("youtube.com") || urlString.contains("youtu.be"))
{
try
{
streamYoutube(urlString, player, messageBuilder);
}
catch(UnsupportedAudioFileException | DiscordException | IOException | RateLimitException | MissingPermissionsException e)
{
logger.error("Error when streaming content from youtube.", e);
}
}
else
{
try
{
URL url = new URL(urlString);
ExtendedTrack track = new ExtendedTrack(new URLProvider(url),
TrackSource.URL, url.toString(), null, null);
player.queue(track);
sendPlayMessage(track, messageBuilder);
}
catch(UnsupportedAudioFileException | IOException e)
{
logger.error("Error when streaming content from url", e);
}
}
}

private static void streamYoutube(String url, AudioPlayer player, MessageBuilder messageBuilder)
throws MissingPermissionsException, IOException, RateLimitException, DiscordException, UnsupportedAudioFileException
{
File resourceFolder = new File("src/main/resources").getAbsoluteFile();
String youtubeDlPath = new File(resourceFolder, "youtube-dl.exe").getPath();
String ffmpegPath = new File(resourceFolder, "ffmpeg.exe").getPath();
final String[] title = new String[1];
final String[] readableDuration = new String[1];
new Thread(() ->
{
ProcessBuilder info = new ProcessBuilder(
youtubeDlPath,
"-q", //quiet. No standard out.
"-j", //Print JSON
"--flat-playlist", //Get ONLY the urls of the playlist if this is a playlist.
"--ignore-errors",
"--skip-download",
"--", url
);

byte[] infoData = new byte[0];
try
{
Process infoProcess = info.start();
infoData = IOUtils.toByteArray(infoProcess.getInputStream());
}
catch(IOException e)
{
e.printStackTrace();
}
if(infoData == null || infoData.length == 0)
{
throw new NullPointerException("The youtube-dl info process returned no data!");
}

String sInfo = new String(infoData);
Scanner scanner = new Scanner(sInfo);

JsonParser parser = new JsonParser();
JsonObject json = parser.parse(scanner.nextLine()).getAsJsonObject();

title[0] = json.has("title")
? json.get("title").getAsString() : (json.has("fulltitle")
? json.get("fulltitle").getAsString() : null);

int durationInSeconds = json.has("duration") ? json.get("duration").getAsInt() : -1;

readableDuration[0] = null;
if(durationInSeconds != -1)
{
readableDuration[0] = String.format(" [%d min, %d sec]",
TimeUnit.SECONDS.toMinutes(durationInSeconds),
TimeUnit.SECONDS.toSeconds(durationInSeconds) -
TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(durationInSeconds))
);
}
}).start();


ProcessBuilder youtube = new ProcessBuilder(
youtubeDlPath,
"-q",
"-f", "mp3/bestaudio/best",
"--no-playlist",
"-o", "-",
"--", url
);
ProcessBuilder ffmpeg = new ProcessBuilder(
ffmpegPath,
"-i", "-",
"-f", "mp3", //Format. mp3
"-movflags", "+faststart",
"-vbr", "4",
"-nostats",
"-preset", "ultrafast",
"-ac", "2", //Channels. Specify 2 for stereo audio.
"-ar", "48000", //Rate. Opus requires an audio rate of 48000hz
"-map", "a", //Makes sure to only output audio, even if the specified format supports other streams
"-"
);

Process yProcess = youtube.start();
Process fProcess = ffmpeg.start();

InputStream from = yProcess.getInputStream();
OutputStream to = fProcess.getOutputStream();

new Thread()
{
@Override
public void run()
{
byte[] buffer = new byte[1024];
int amountRead = -1;
try
{
while(!isInterrupted() && ((amountRead = from.read(buffer)) > -1))
{
to.write(buffer, 0, amountRead);
}
to.flush();

from.close();
to.close();

yProcess.destroy();
}
catch(IOException e)
{
if(!e.getMessage().contains("The pipe has been ended"))
{
logger.error("While fetching music", e);
}
}
}
}.start();

new Thread("youtube-dl ErrorStream")
{
@Override
public void run()
{
try
{
InputStream fromYTDL = null;

fromYTDL = yProcess.getErrorStream();
if(fromYTDL == null)
{
logger.error("youtube-dl ErrorStream is null");
}

byte[] buffer = new byte[1024];
int amountRead = -1;
while(!isInterrupted() && ((amountRead = fromYTDL.read(buffer)) > -1))
{
logger.warn("youtube-dl error: " + new String(Arrays.copyOf(buffer, amountRead)));
}
}
catch(IOException e)
{
logger.debug("youtube-dl", e);
}
}
}.start();

ExtendedTrack track = new ExtendedTrack(
AudioSystem.getAudioInputStream(fProcess.getInputStream()),
TrackSource.YOUTUBE, url, title[0], readableDuration[0]);

player.queue(track);

sendPlayMessage(track, messageBuilder);
}

private static void sendPlayMessage(AudioPlayer.Track track, MessageBuilder messageBuilder)
{
try
{
messageBuilder
.appendContent("Queued ")
.appendContent(track.toString(), MessageBuilder.Styles.BOLD)
.build();
}
catch(DiscordException |RateLimitException | MissingPermissionsException e)
{
e.printStackTrace();
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/github/langebangen/kensa/Command.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.github.langebangen.kensa;

/**
* @author langen
*/
public class Command
{
private final Action action;
private final String argument;

public Command(Action action, String argument)
{
this.action = action;
this.argument = argument;
}

public Action getAction()
{
return action;
}

public String getArgument()
{
return argument;
}

public static Command parseCommand(String value)
{
if(!value.isEmpty())
{
String[] commands = value.split(" ");
String actionString = commands[0];
Action action = Action.getAction(actionString);
if(action != null)
{
return new Command(action, commands.length > 1
? value.substring(1 + actionString.length() + value.indexOf(commands[0]))
: null);
}
}

return null;
}
}
Loading

0 comments on commit f2474ee

Please sign in to comment.