Compare commits

...

55 Commits

Author SHA1 Message Date
Nekojimi 5ec62d4d43 Add support for uploading tracks to play them. 9 months ago
Nekojimi a9c5bb37f8 Disable streaming mode for now. 9 months ago
Nekojimi 6759afe76b WIP: initial work on streaming instead of downloading. 9 months ago
Nekojimi c8684dbe75 Fix exception raised if something is added to the queue while Chords isn't present. 9 months ago
Nekojimi 11c8991943 Fix track skipping not clearing the current track. 9 months ago
Nekojimi 95d2e41a03 Add support for Tracks to store new input stream formats, not just files. 9 months ago
Nekojimi 536743c6c1 MusicHandler: properly skip tracks on error. 9 months ago
Nekojimi 143f5dacda Prevent error if queue command is used while not in a channel. 9 months ago
Nekojimi 758f6a0d48 Fix issue where no URL would be retrieved for search results. 9 months ago
Nekojimi a901163af3 Retry opening audio streams after all exceptions, not just IO failure. 9 months ago
Nekojimi abd676fa49 Retry opening track files after failure, and gracefully handle final failure by skipping the track. 9 months ago
Nekojimi dd977cc899 Fuck my entire existence 9 months ago
Nekojimi 1d78f2a3fb Download 8 fragments in parallel. 9 months ago
Nekojimi c2c748470f Possibly optimise download speed by rate-limiting update messages? 9 months ago
Nekojimi 268af0ff3f Stop pretending to be Android. 9 months ago
Nekojimi 26010946c2 Temporarily fix Downloader. 9 months ago
Nekojimi ab26e65fdb Remove bad references to Recommender. 9 months ago
Nekojimi 571139a59c Screw it, more changes 9 months ago
Nekojimi 95f838c392 Fix play command search result selection. 10 months ago
Nekojimi 7d3795b10a Remove debug audio output. 11 months ago
Nekojimi 3899792e7f Update README.md 11 months ago
Nekojimi fdbd8937f5 Change pipeline to use pull architecture. 11 months ago
Nekojimi b41f5c97a3 Delete 'pom.xml.orig' 1 year ago
Nekojimi 1ad6617914 Update '.gitignore' 1 year ago
Nekojimi 16c2c61e76 Fix auto-disconnect when last user leaves. 1 year ago
Nekojimi 2a25e1368d Mega-refactor: change song to track everywhere. 1 year ago
Nekojimi abfab9a18d Clean up code. 1 year ago
Nekojimi 07b45309c1 Ensure tracks play in the correct order by using PriorityQueue. 1 year ago
Nekojimi 310158a39b Add support for downloading entire playlists. 1 year ago
Nekojimi de807b4524 Add equals and hashcode to Format. 1 year ago
Nekojimi d9a7da27f2 Fix issue with skip command not ending current track. 1 year ago
Nekojimi be6036cd5d Merge branch 'jda-v5' 2 years ago
Nekojimi c7ab883d5e Update JDA to v5.0. 2 years ago
Nekojimi 3ff544d699 Report download progress while downloading. 2 years ago
Nekojimi 9fa5e36cdc Handle new format listing style in yt-dlp. 2 years ago
Nekojimi d50657ef90 Move local address handling to first part of initialisation. 2 years ago
Nekojimi f1cc31dcd9 Send acknowledgement message for new songs immediately, instead of after download is scheduled. 2 years ago
Nekojimi 0914cadaf5 Move some text-parsing methods to Util class. 2 years ago
Nekojimi 490101fcad Update JDA to v5.0. 2 years ago
Nekojimi 1dba55620f Print local address to debug on start. 2 years ago
Nekojimi 0c29d56317 Made LocalBindSocketFactory much more hardcore. 2 years ago
Nekojimi 6ef49c9e51 Fixed some silly mistakes with local address handling. 2 years ago
Nekojimi 7c4ea13e3c Change local address handling to also apply to JDA's HTTPClient. 2 years ago
Nekojimi 7cd118985a Add new settings system, and allow youtube-dl command to be set via it. 2 years ago
Nekojimi 2ec6da49b5 Fix main class in pom.xml. 2 years ago
Nekojimi f043ab906d Add debug warning if music player starts pulling tricky shit again 2 years ago
Nekojimi 6f23b60511 Update references to main class. 2 years ago
Nekojimi 1b7d4adeb9 Rename main class to "Chords". 2 years ago
Nekojimi 000e0e5d5c Fix reading of API token. 2 years ago
Nekojimi 52e34493bd Add JCommander to dependencies. 2 years ago
Nekojimi 824d08caad Add command line option to bind to a local socket. Also, add command line parsing. 2 years ago
Nekojimi b0415b0324 Downloader: link preferred bitrate to JDA requested format. 3 years ago
Nekojimi ae7fe8cc7b Add back support for restarting the current track. 3 years ago
Nekojimi 4e654d2d18 Read format info from Youtube JSON response. 3 years ago
Nekojimi ca3ebd712b Add new invocation system to command structure. 3 years ago
  1. 4
      .gitignore
  2. 11
      README.md
  3. BIN
      chords.png
  4. BIN
      chords.xcf
  5. 1
      nb-configuration.xml
  6. 6
      nbactions.xml
  7. 9
      pom.xml
  8. 3
      settings.yml
  9. 556
      src/main/java/moe/nekojimi/chords/Chords.java
  10. 52
      src/main/java/moe/nekojimi/chords/CommandOptions.java
  11. 506
      src/main/java/moe/nekojimi/chords/Downloader.java
  12. 133
      src/main/java/moe/nekojimi/chords/Format.java
  13. 112
      src/main/java/moe/nekojimi/chords/LocalBindSocketFactory.java
  14. 417
      src/main/java/moe/nekojimi/chords/Main.java
  15. 249
      src/main/java/moe/nekojimi/chords/MusicHandler.java
  16. 42
      src/main/java/moe/nekojimi/chords/Playlist.java
  17. 134
      src/main/java/moe/nekojimi/chords/QueueManager.java
  18. 211
      src/main/java/moe/nekojimi/chords/QueueThing.java
  19. 59
      src/main/java/moe/nekojimi/chords/Settings.java
  20. 107
      src/main/java/moe/nekojimi/chords/Track.java
  21. 42
      src/main/java/moe/nekojimi/chords/TrackPlayer.java
  22. 210
      src/main/java/moe/nekojimi/chords/TrackRequest.java
  23. 58
      src/main/java/moe/nekojimi/chords/Util.java
  24. 25
      src/main/java/moe/nekojimi/chords/commands/Command.java
  25. 29
      src/main/java/moe/nekojimi/chords/commands/HelpCommand.java
  26. 88
      src/main/java/moe/nekojimi/chords/commands/Invocation.java
  27. 53
      src/main/java/moe/nekojimi/chords/commands/JoinCommand.java
  28. 8
      src/main/java/moe/nekojimi/chords/commands/LeaveCommand.java
  29. 49
      src/main/java/moe/nekojimi/chords/commands/PlayCommand.java
  30. 50
      src/main/java/moe/nekojimi/chords/commands/QueueCommand.java
  31. 20
      src/main/java/moe/nekojimi/chords/commands/RemoveCommand.java
  32. 14
      src/main/java/moe/nekojimi/chords/commands/RestartCommand.java
  33. 14
      src/main/java/moe/nekojimi/chords/commands/SkipCommand.java

4
.gitignore vendored

@ -55,5 +55,9 @@ dist/
nbdist/ nbdist/
.nb-gradle/ .nb-gradle/
# Git probably
*.orig
# Secrets! # Secrets!
secrets.yml secrets.yml
settings.yml

@ -1,2 +1,11 @@
# Chords # Chords - Yet Another Discord Music Bot
![The Chords logo - a white guitar with the word "Chords" in bubble font, on a blue background.](chords.png)
Basically everyone has written one at this point, but our little community needed one, so I made one!
Here's some stuff about it:
- Can play from any site that youtube-dl supports ([that's over 1800!](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
- Built-in multi-site searching via my own [MusicSearcher](https://fluff.nekojimi.moe/gitea/Nekojimi/MusicSearcher) project
- More cool stuff coming soon!

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

@ -14,5 +14,6 @@ That way multiple projects can share the same settings (useful for formatting ru
Any value defined here will override the pom.xml file value but is only applicable to the current project. Any value defined here will override the pom.xml file value but is only applicable to the current project.
--> -->
<netbeans.hint.license>gpl30</netbeans.hint.license> <netbeans.hint.license>gpl30</netbeans.hint.license>
<org-netbeans-modules-javascript2-requirejs.enabled>true</org-netbeans-modules-javascript2-requirejs.enabled>
</properties> </properties>
</project-shared-configuration> </project-shared-configuration>

@ -10,7 +10,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal> <goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals> </goals>
<properties> <properties>
<exec.args>-classpath %classpath moe.nekojimi.chords.Main ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args> <exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.executable>java</exec.executable> <exec.executable>java</exec.executable>
</properties> </properties>
</action> </action>
@ -24,7 +24,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal> <goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals> </goals>
<properties> <properties>
<exec.args>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath moe.nekojimi.chords.Main ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args> <exec.args>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.executable>java</exec.executable> <exec.executable>java</exec.executable>
<jpda.listen>true</jpda.listen> <jpda.listen>true</jpda.listen>
</properties> </properties>
@ -39,7 +39,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal> <goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals> </goals>
<properties> <properties>
<exec.args>-classpath %classpath ${packageClassName} ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args> <exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.executable>java</exec.executable> <exec.executable>java</exec.executable>
</properties> </properties>
</action> </action>

@ -20,7 +20,7 @@
<archive> <archive>
<manifest> <manifest>
<mainClass> <mainClass>
moe.nekojimi.chords.Main moe.nekojimi.chords.Chords
</mainClass> </mainClass>
</manifest> </manifest>
</archive> </archive>
@ -35,7 +35,7 @@
<dependency> <dependency>
<groupId>net.dv8tion</groupId> <groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId> <artifactId>JDA</artifactId>
<version>4.3.0_277</version> <version>5.0.0-beta.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
@ -54,6 +54,11 @@
<version>1.1.4</version> <version>1.1.4</version>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.78</version>
</dependency>
</dependencies> </dependencies>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

@ -0,0 +1,3 @@
ytdl-cmd: /usr/bin/yt-dlp
discord-token: ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk
local-addr: 192.168.1.84

@ -0,0 +1,556 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlMapping;
import com.beust.jcommander.JCommander;
import com.neovisionaries.ws.client.WebSocketFactory;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.login.LoginException;
import moe.nekojimi.chords.commands.*;
import moe.nekojimi.musicsearcher.providers.MetaSearcher;
import moe.nekojimi.musicsearcher.providers.Searcher;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import net.dv8tion.jda.api.events.message.MessageEmbedEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.Compression;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import net.dv8tion.jda.internal.utils.IOUtil;
import okhttp3.OkHttpClient;
/**
* The Chords music Discord bot.
*
* @author Nekojimi
*/
public final class Chords extends ListenerAdapter
{
private static final CommandOptions options = new CommandOptions();
private static Settings settings;
private final File dataDirectory;
private final File playlistsDirectory;
private MusicHandler musicHandler;
private final Downloader downloader;
private final Searcher searcher;
private final QueueManager queueManager;
// private Recommender recommender;
private JDA jda;
private final Map<String, Command> commands = new HashMap<>();
private final Command helpCommand;
private PlayCommand playCommand;
private AudioChannel currentVoiceChannel = null;
private final Map<String, Playlist> playlists = new HashMap<>();
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws LoginException, IOException
{
JCommander commander = JCommander.newBuilder()
.args(args)
.acceptUnknownOptions(true)
.addObject(options)
.build();
// We only need 2 gateway intents enabled for this example:
EnumSet<GatewayIntent> intents = EnumSet.of(
// We need messages in guilds to accept commands from users
GatewayIntent.GUILD_MESSAGES,
// We need voice states to connect to the voice channel
GatewayIntent.GUILD_VOICE_STATES,
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.MESSAGE_CONTENT
);
settings = new Settings(new File(options.getSettingsPath()));
JDABuilder builder = JDABuilder.createDefault(settings.getDiscordToken(), intents);
builder.enableIntents(GatewayIntent.MESSAGE_CONTENT);
if (settings.getLocalAddr() != null)
{
System.out.println("Using local address: " + settings.getLocalAddr());
// make local binding socket factory
final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory();
localBindSocketFactory.setLocalAddress(InetAddress.getByName(settings.getLocalAddr()));
// install local socket factory for HTTP
OkHttpClient.Builder httpBuilder = IOUtil.newHttpClientBuilder();
httpBuilder.socketFactory(localBindSocketFactory);
builder.setHttpClientBuilder(httpBuilder);
// install local socket factory for websockets
final WebSocketFactory webSocketFactory = new WebSocketFactory();
webSocketFactory.setSocketFactory(localBindSocketFactory);
builder.setWebsocketFactory(webSocketFactory);
}
// Disable parts of the cache
builder.disableCache(CacheFlag.MEMBER_OVERRIDES);
// Enable the bulk delete event
builder.setBulkDeleteSplittingEnabled(false);
// Disable compression (not recommended)
builder.setCompression(Compression.NONE);
// Set activity (like "playing Something")
builder.setActivity(Activity.playing("music!"));
final Chords listener = new Chords();
builder.addEventListeners(listener);
builder.setAutoReconnect(true);
JDA jda = builder.build();
listener.setJda(jda);
}
private final Consumer<Track> nowPlayingConsumer = new NowPlayingConsumer();
private final BiConsumer<TrackRequest, Exception> downloaderMessageHandler = new DownloaderMessageHandler();
public Chords() throws IOException
{
log("INFO", "Starting up...");
// init dirs
dataDirectory = new File(System.getProperty("user.dir"));
playlistsDirectory = initDirectory(dataDirectory, "playlists");
// init downloader
downloader = new Downloader();
downloader.setMessageHandler(downloaderMessageHandler);
// init searcher
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
// init queue manager
queueManager = new QueueManager();
queueManager.addSource(downloader);
// init commands
addCommand(new JoinCommand(this));
addCommand(new LeaveCommand(this));
playCommand = new PlayCommand(this);
addCommand(playCommand);
addCommand(new QueueCommand(this));
addCommand(new RemoveCommand(this));
addCommand(new RestartCommand(this));
addCommand(new SkipCommand(this));
helpCommand = new HelpCommand(this);
addCommand(helpCommand);
// load playlists
loadPlaylists();
log("INFO", "Started OK!");
}
private void addCommand(Command command)
{
commands.put(command.getKeyword(), command);
}
public void setJda(JDA jda)
{
this.jda = jda;
}
@Override
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event)
{
super.onGuildVoiceUpdate(event);
if (this.currentVoiceChannel == null)
return;
final AudioChannel channelLeft = event.getChannelLeft();
if (channelLeft != null && channelLeft == currentVoiceChannel)
{
if (channelLeft.getMembers().size() <= 1) // just us now
disconnect();
}
}
@Override
public void onMessageReceived(MessageReceivedEvent event)
{
super.onMessageReceived(event); //To change body of generated methods, choose Tools | Templates.
Message message = event.getMessage();
User author = message.getAuthor();
String content = message.getContentRaw();
Guild guild = event.getGuild();
// Ignore message if it's not in a music channel
if (!event.getChannel().getName().contains("music"))
return;
// Ignore message if bot
if (author.isBot())
return;
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content);
Invocation invocation = null;
try
{
try
{
URL parseURL = new URL(content.trim());
invocation = new Invocation(event, List.of(parseURL.toExternalForm()));
playCommand.call(invocation);
return;
} catch (MalformedURLException ex)
{
// not a URL, then
}
String[] split = content.split("\\s+");
List<String> args = new ArrayList<>();
Collections.addAll(args, split);
args.remove(0);
invocation = new Invocation(event, args);
invocation.setRequestMessage(message);
String cmd = split[0].toLowerCase();
if (cmd.startsWith("!"))
{
cmd = cmd.substring(1); // strip prefix char
if (commands.containsKey(cmd))
{
Command command = commands.get(cmd);
command.call(invocation);
} else
{
helpCommand.call(invocation);
}
} else if (!event.getMessage().getAttachments().isEmpty())
{
for (Message.Attachment attach : event.getMessage().getAttachments())
{
if (attach.getContentType().startsWith("audio") || attach.getContentType().startsWith("video"))
{
try
{
invocation = new Invocation(event, List.of(new URL(attach.getUrl()).toExternalForm()));
invocation.setRequestMessage(message);
playCommand.call(invocation);
} catch (MalformedURLException ex)
{
Logger.getLogger(Chords.class.getName()).log(Level.WARNING, null, ex);
}
}
}
}
} catch (Exception ex)
{
if (invocation != null)
invocation.respond("Error: " + ex.getMessage());
else
event.getChannel().sendMessage("Error: " + ex.getMessage()).queue();
log("UERR", "Command error:" + ex.getMessage());
}
// TODO: this will handle uploading files, maybe
}
public void queueDownload(TrackRequest request)
{
request.getInvocation().respond("Request pending...");
downloader.accept(request);
}
public void setStatus(Track nowPlaying)
{
jda.getPresence().setActivity(Activity.listening(nowPlaying.toString()));
}
public void clearStatus()
{
jda.getPresence().setActivity(null);
}
/**
* Connect to requested channel and start echo handler
*
* @param channel
* The channel to connect to
*/
public void connectTo(AudioChannel channel)
{
Guild guild = channel.getGuild();
// Get an audio manager for this guild, this will be created upon first use for each guild
AudioManager audioManager = guild.getAudioManager();
musicHandler = new MusicHandler();
queueManager.setHandler(musicHandler);
// Create our Send/Receive handler for the audio connection
// EchoHandler handler = new EchoHandler();
// The order of the following instructions does not matter!
// Set the sending handler to our echo system
audioManager.setSendingHandler(musicHandler);
// Set the receiving handler to the same echo system, otherwise we can't echo anything
// audioManager.setReceivingHandler(handler);
// Connect to the voice channel
audioManager.openAudioConnection(channel);
currentVoiceChannel = channel;
musicHandler.setNowPlayingConsumer(nowPlayingConsumer);
}
public void disconnect()
{
if (currentVoiceChannel != null)
{
Guild guild = currentVoiceChannel.getGuild();
AudioManager audioManager = guild.getAudioManager();
audioManager.setSendingHandler(null);
audioManager.closeAudioConnection();
musicHandler = null;
currentVoiceChannel = null;
}
}
public void log(String type, String message)
{
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message);
}
public File initDirectory(File parent, String name) throws IOException
{
File ret = new File(parent, name);
if (!ret.exists())
Files.createDirectories(ret.toPath());
if (!ret.canRead())
throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!");
if (!ret.canWrite())
throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!");
return ret;
}
private void loadPlaylists()
{
File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml"));
for (File file : files)
{
try
{
YamlMapping map = Yaml.createYamlInput(file).readYamlMapping();
Playlist playlist = Playlist.fromYaml(map);
playlists.put(playlist.getName(), playlist);
} catch (IOException ex)
{
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public void setRecommenderEnabled(boolean enabled)
{
// if (recommender == null && enabled)
// {
//// recommender = new Recommender();
// downloader.addSource(recommender);
// } else if (recommender != null && !enabled)
// {
//// downloader.removeSource(recommender);
// recommender = null;
// }
}
public MusicHandler getMusicHandler()
{
return musicHandler;
}
public Downloader getDownloader()
{
return downloader;
}
public Searcher getSearcher()
{
return searcher;
}
public JDA getJda()
{
return jda;
}
public AudioChannel getCurrentVoiceChannel()
{
return currentVoiceChannel;
}
public QueueManager getQueueManager()
{
return queueManager;
}
public static Settings getSettings()
{
return settings;
}
public static CommandOptions getOptions()
{
return options;
}
public File getDataDirectory()
{
return dataDirectory;
}
public File getPlaylistsDirectory()
{
return playlistsDirectory;
}
public Map<String, Command> getCommands()
{
return commands;
}
public Map<String, Playlist> getPlaylists()
{
return playlists;
}
// public Recommender getRecommender()
// {
// return recommender;
// }
private class DownloaderMessageHandler implements BiConsumer<TrackRequest, Exception>
{
private static final String PROGRESS_SYMBOLS = " ▏▎▍▌▋▊▉█";
private double lastProgressUpdate = 0.0;
private boolean initialUpdateSent = false;
/*
🌑🌘🌗🌖🌕
🌩🌨🌧🌦🌥🌤🌞
🞎🞏🞐🞑🞒🞓
*/
// private static final String PROGRESS_SYMBOLS = " ⠀⡀⣀⣠⣤⣦⣶⣾⣿";
public DownloaderMessageHandler()
{
}
@Override
public void accept(TrackRequest request, Exception ex)
{
boolean shouldUpdate = false;
String response = "";
if (request.getTracks().size() > 1)
response += "Downloading " + request.getTracks().size() + " tracks:\n";
for (Track track : request.getTracks())
{
// TextChannel channel = jda.getTextChannelById(track.getRequestedIn());
// String bracketNo = "[" + track.getNumber() + "] ";
if (ex == null)
if (track.getLocation() != null && track.getProgress() >= 100)
{
response += ("Finished downloading " + track + "!");
log("DOWN", "Downloaded " + track);
shouldUpdate = true;
} else
{
Format format = track.getBestFormat();
String formatDetails = "";
if (format != null)
{
final int bitrate = format.getSampleRate() / 1000;
final long size = format.getSize();
String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB";
String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k";
formatDetails = " (" + bitFmt + ", " + sizeFmt + ")";
}
String progressDetails = "";
if (track.getProgress() >= 0)
{
if (track.getProgress() >= lastProgressUpdate + 5.0)
{
shouldUpdate = true;
lastProgressUpdate = track.getProgress();
}
progressDetails = " [" + String.format("%.1f", track.getProgress()) + "%]";
}
response += ("Now downloading " + track + formatDetails + progressDetails + " ...");
log("DOWN", "Downloading " + track + "...");
}
else
{
response += ("Failed to download " + track + "! Reason: " + ex.getMessage());
log("DOWN", "Failed to download " + track + "! Reason: " + ex.getMessage());
shouldUpdate = true;
}
response += "\n";
}
if (!response.isEmpty() && (shouldUpdate || !initialUpdateSent))
{
request.getInvocation().respond(response);
initialUpdateSent = true;
}
}
}
private class NowPlayingConsumer implements Consumer<Track>
{
public NowPlayingConsumer()
{
}
@Override
public void accept(Track track)
{
if (track != null)
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, track.toString()));
else
jda.getPresence().setActivity(null);
// if (recommender != null)
// recommender.addSeed(track);
}
}
}

@ -0,0 +1,52 @@
/*
* Copyright (C) 2022 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import com.beust.jcommander.Parameter;
/**
*
* @author jimj316
*/
public class CommandOptions
{
@Parameter(names = "-settings", description = "A path to the settings.yml file.")
private String settingsPath = "settings.yml";
// @Parameter(names = "-token", description = "The API token for Discord.", required = true)
// private String token;
//
// @Parameter(names = "-local-addr", description = "The local address to bind to.")
// private String localAddress;
// public String getToken()
// {
// return token;
// }
//
// public String getLocalAddress()
// {
// return localAddress;
// }
public String getSettingsPath()
{
return settingsPath;
}
}

@ -5,108 +5,134 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.io.File; import com.beust.jcommander.Strings;
import java.io.IOException; import java.io.*;
import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.*;
import java.util.LinkedList; import java.util.concurrent.*;
import java.util.List;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject; import javax.json.JsonObject;
import javax.json.JsonReader; import javax.json.JsonReader;
import moe.nekojimi.chords.Downloader.DownloadTask; import moe.nekojimi.musicsearcher.Result;
import net.dv8tion.jda.api.audio.AudioSendHandler;
/** /**
* *
* @author jimj316 * @author jimj316
*/ */
public class Downloader implements Consumer<DownloadTask> public class Downloader extends QueueThing<TrackRequest, Track>
{ {
private static final int BITRATE_TARGET = 64_000; private static final int DOWNLOAD_TIMEOUT = 300;
private static final int INFO_TIMEOUT = 60;
private static final int FORMAT_TIMEOUT = 5;
private static final int BITRATE_TARGET = (int) AudioSendHandler.INPUT_FORMAT.getSampleRate();
private static final Pattern FORMAT_PATTERN = Pattern.compile("^([\\w]+)\\s+([\\w]+)\\s+(\\w+ ?\\w*)\\s+(.*)$"); private static final Pattern FORMAT_PATTERN = Pattern.compile("^([\\w]+)\\s+([\\w]+)\\s+(\\w+ ?\\w*)\\s+(.*)$");
public static final Pattern DESTINATION_PATTERN = Pattern.compile("Destination: (.*\\.wav)"); public static final Pattern DESTINATION_PATTERN = Pattern.compile("Destination: (.*\\.wav)");
public static final Pattern STREAM_PATTERN = Pattern.compile("Destination: -");
public static final Pattern PROGRESS_PATTERN = Pattern.compile("\\[download\\].*?([\\d\\.]+)%");
private static final Pattern INFO_JSON_PATTERN = Pattern.compile("Writing video metadata as JSON to: (.*\\.info\\.json)");
private static final Pattern ETA_PATTERN = Pattern.compile("\\[download\\].*?ETA\\s+(\\d{1,2}:\\d{2})");
private static final Pattern DOWNLOAD_ITEM_PATTERN = Pattern.compile("\\[download\\] Downloading item (\\d+) of (\\d+)");
public static final String[] INFO_TITLE_KEYS =
{
"track", "title", "fulltitle"
};
public static final String[] INFO_ARTIST_KEYS =
{
"artist", "channel", "uploader"
};
private final List<DownloadTask> downloadQueue = new LinkedList<>(); private final List<TrackRequest> downloadingTracks = new LinkedList<>();
private final LinkedBlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>(); private final LinkedBlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>();
private final ThreadPoolExecutor exec = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue); private final ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue);
// private Consumer<Song> next; private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
private BiConsumer<SongRequest, Exception> messageHandler; // private Consumer<Track> next;
private BiConsumer<TrackRequest, Exception> messageHandler;
private File downloadDir = null; private File downloadDir = null;
private int trackNumber = 1;
public Downloader() public Downloader()
{ {
super(new LinkedList<>());
} }
//
// @Override
// public void accept(TrackRequest request)
// {
// if all tracks of the request are already downloaded, just skip
// if (!request.getTracks().isEmpty() && request.getTracks().stream().allMatch((t) -> t.isDownloaded()))
// {
// for (Track track : request.getTracks())
// getDestination().accept(track);
// return;
// }
// inputQueue.add(task);
// }
@Override @Override
public void accept(DownloadTask task) protected boolean completePromise(Promise<TrackRequest, Track> promise)
{ {
// if already downloaded, just skip TrackRequest request = promise.getInput();
Song song = task.request.getSong(); if (request == null)
if (song.isDownloaded()) return false;
{
task.getDestination().accept(song);
return;
}
downloadQueue.add(task); executor.submit(() ->
getInfo(song);
exec.submit(() ->
{ {
downloadingTracks.add(request);
try try
{ {
getFormats(song);
download(task); getInfo(request);
// boolean streamOutput = request.getTracks().size() == 1;
boolean streamOutput = false;
download(promise, streamOutput);
} catch (Exception ex) } catch (Exception ex)
{ {
ex.printStackTrace(); ex.printStackTrace();
} }
downloadingTracks.remove(request);
}); });
return true;
} }
private void chooseFormats(Song song) private List<Format> sortFormats(Collection<Format> input)
{ {
List<Format> formats = song.getFormats(); List<Format> formats = new ArrayList<>(input);
if (formats.isEmpty())
return;
// System.out.println("Choosing from " + formats.size() + " formats:");
// System.out.println(formats);
formats.sort((Format a, Format b) -> formats.sort((Format a, Format b) ->
{ {
// audio only preferred to video // audio only preferred to video
// System.out.println("sort entered; a=" + a.toString() + " b=" + b.toString());
int comp = 0; int comp = 0;
comp = Boolean.compare(a.isAudioOnly(), b.isAudioOnly()); comp = Boolean.compare(a.isAudioOnly(), b.isAudioOnly());
// System.out.println("\tCompared on audio only: " + comp);
if (comp == 0) if (comp == 0)
{ {
// known preferred to unknown // known preferred to unknown
if (a.getBitrate() == b.getBitrate()) if (a.getSampleRate() == b.getSampleRate())
comp = 0; comp = 0;
else if (a.getBitrate() <= 0) else if (a.getSampleRate() <= 0)
comp = 1; comp = 1;
else if (b.getBitrate() <= 0) else if (b.getSampleRate() <= 0)
comp = -1; comp = -1;
else // closer to the target bitrate is best else // closer to the target bitrate is best
{ {
int aDist = Math.abs(a.getBitrate() - BITRATE_TARGET); int aDist = Math.abs(a.getSampleRate() - BITRATE_TARGET);
int bDist = Math.abs(b.getBitrate() - BITRATE_TARGET); int bDist = Math.abs(b.getSampleRate() - BITRATE_TARGET);
comp = Integer.compare(bDist, aDist); comp = Integer.compare(bDist, aDist);
// System.out.println("\tCompared on bitrate distance: " + comp);
} }
} }
if (comp == 0) if (comp == 0)
@ -114,53 +140,85 @@ public class Downloader implements Consumer<DownloadTask>
// known preferred to unknown // known preferred to unknown
if (a.getSize() == b.getSize()) if (a.getSize() == b.getSize())
comp = 0; comp = 0;
if (a.getSize() <= 0) else if (a.getSize() <= 0)
comp = 1; comp = 1;
else if (b.getSize() <= 0) else if (b.getSize() <= 0)
comp = -1; comp = -1;
else // smaller is better else // smaller is better
{
comp = Long.compare(b.getSize(), a.getSize()); comp = Long.compare(b.getSize(), a.getSize());
// System.out.println("\tCompared on filesize: " + comp);
}
} }
// System.out.println("\tOverall: " + comp);
return -comp; return -comp;
}); });
song.setFormats(formats); return formats;
// System.out.println("Sorting done! Formats:" + formats);
} }
private void getFormats(Song song) private void getFormats(Track track)
{ {
// if (!track.getFormats().isEmpty())
return;
try try
{ {
String cmd = "/usr/bin/youtube-dl --skip-download -F " + song.getUrl().toString(); // String cmd = + " --skip-download -F " + track.getUrl().toString();
Process exec = runCommand(cmd, 5); Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "-F", track.getUrl().toString()), FORMAT_TIMEOUT);
InputStream input = exec.getInputStream(); InputStream input = exec.getInputStream();
String output = new String(input.readAllBytes(), Charset.defaultCharset()); String output = new String(input.readAllBytes(), Charset.defaultCharset());
List<Format> formats = new ArrayList<>(); List<Format> formats = new ArrayList<>();
int codeCol = 0;
int extCol = 1;
int resCol = 2;
int noteCol = 3;
int sizeCol = -1;
int bitrateCol = -1;
List<String> list = output.lines().collect(Collectors.toList()); List<String> list = output.lines().collect(Collectors.toList());
int i = 0; int i = 0;
while (!list.get(i).contains("Available formats")) while (!list.get(i).contains("Available formats"))
i++; i++;
i++; i++;
if (list.get(i).contains("FILESIZE"))
{
String[] split = list.get(i).split("\\s+");
for (int j = 0; j < split.length; j++)
{
switch (split[j])
{
case "ID":
codeCol = j;
break;
case "EXT":
extCol = j;
break;
case "RESOLUTION":
resCol = j;
break;
case "MORE":
noteCol = j;
break;
case "FILESIZE":
sizeCol = j;
break;
case "TBR":
bitrateCol = j;
}
}
i += 2;
}
for (; i < list.size(); i++) for (; i < list.size(); i++)
{ {
String line = list.get(i); String line = list.get(i);
String[] split = line.split("\\s\\s+", 4); String[] split = line.split("\\s+", Math.max(4, noteCol - 1));
if (split.length < 4) if (split.length < 4)
continue; continue;
formats.add(new Format(split[0], split[1], split[2], split[3])); final Format format = new Format(split[codeCol], split[extCol], split[resCol], split[noteCol]);
if (sizeCol >= 0)
format.setSize(Util.parseSize(split[sizeCol]));
if (bitrateCol >= 0)
format.setSampleRate(Util.parseSampleRate(split[bitrateCol]));
formats.add(format);
} }
song.setFormats(formats); track.setFormats(formats);
// Matcher matcher = FORMAT_PATTERN.matcher(output);
// while (matcher.find())
// formats.add(new Format(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4)));
// return formats;
} catch (Exception ex) } catch (Exception ex)
{ {
@ -169,23 +227,88 @@ public class Downloader implements Consumer<DownloadTask>
} }
} }
private void getInfo(Song song) private List<Track> getInfo(TrackRequest request)
{ {
List<Track> ret = new ArrayList<>();
try try
{ {
String cmd = "/usr/bin/youtube-dl --skip-download --print-json " + song.getUrl().toString(); // String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download --print-json " + request.getUrl().toString();
Process exec = runCommand(cmd, 5); Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "--print-json", request.getUrl().toString()), INFO_TIMEOUT);
InputStream input = exec.getInputStream(); InputStream input = exec.getInputStream();
JsonReader reader = Json.createReader(input);
JsonObject object = reader.readObject(); // read each line as JSON, turn each into a track object
if (song.getTitle() == null) Scanner sc = new Scanner(input);
song.setTitle(object.getString("title", null)); while (sc.hasNextLine())
if (song.getArtist() == null) {
song.setArtist(object.getString("uploader", null)); Track track = new Track(request);
track.setNumber(trackNumber);
trackNumber++;
request.addTrack(track);
String line = sc.nextLine();
JsonReader reader = Json.createReader(new StringReader(line));
JsonObject object = reader.readObject();
Result result = request.getResult();
// look for metadata
if (track.getTitle() == null)
{
if (result != null && !result.getTitle().isBlank())
track.setTitle(result.getTitle());
for (String key : INFO_TITLE_KEYS)
{
if (object.containsKey(key) && !object.getString(key).isBlank())
{
track.setTitle(object.getString(key));
break;
}
}
}
if (track.getArtist() == null)
{
if (result != null && !result.getArtist().isBlank())
track.setArtist(result.getArtist());
for (String key : INFO_ARTIST_KEYS)
{
if (object.containsKey(key) && !object.getString(key).isBlank())
{
track.setArtist(object.getString(key));
break;
}
}
}
if (track.getTitle().contains("-"))
{
String[] split = track.getTitle().split("-", 2);
track.setArtist(split[0]);
track.setTitle(split[1]);
}
if (track.getArtist().contains(" - Topic"))
{
track.setArtist(track.getArtist().replace(" - Topic", ""));
}
JsonArray formatsJSON = object.getJsonArray("formats");
if (formatsJSON != null)
{
List<Format> formats = new ArrayList<>();
for (JsonObject formatJson : formatsJSON.getValuesAs(JsonObject.class))
{
Format format = Format.fromJSON(formatJson);
if (format != null)
formats.add(format);
}
track.setFormats(formats);
}
ret.add(track);
}
} catch (Exception ex) } catch (Exception ex)
{ {
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
} }
return ret;
} }
private File getDownloadDir() throws IOException private File getDownloadDir() throws IOException
@ -195,101 +318,216 @@ public class Downloader implements Consumer<DownloadTask>
return downloadDir; return downloadDir;
} }
private void download(DownloadTask task) private Track getTrackFromRequest(TrackRequest request, int idx)
{ {
Song song = task.request.getSong(); // if there's less tracks in the request than expected, fill the array
chooseFormats(song); while (idx >= request.getTracks().size())
{
Track track = new Track(request);
track.setNumber(trackNumber);
trackNumber++;
request.addTrack(track);
}
return request.getTracks().get(idx);
}
private void download(Promise<TrackRequest, Track> promise, boolean streamOutput) throws InterruptedException, ExecutionException
{
TrackRequest request = promise.getInput();
Set<Format> uniqueFormats = new HashSet<>();
for (Track track : request.getTracks())
{
uniqueFormats.addAll(track.getFormats());
}
List<Format> sortedFormats = sortFormats(uniqueFormats);
String formatCodes = ""; String formatCodes = "";
final List<Format> formats = song.getFormats(); final List<Format> formats = sortedFormats;
for (int i = 0; i < 3 && i < song.getFormats().size(); i++) for (int i = 0; i < 5 && i < sortedFormats.size(); i++)
formatCodes += formats.get(i).getCode() + "/"; formatCodes += formats.get(i).getCode() + "/";
int downloadIdx = 0;
try try
{ {
messageHandler.accept(task.request, null); messageHandler.accept(request, null);
String cmd = "/usr/bin/youtube-dl -x" List<String> cmd = new ArrayList<>();
+ " -f " + formatCodes + "worstaudio/bestaudio/worst/best" cmd.add(Chords.getSettings().getYtdlCommand());
+ " --audio-format=wav" cmd.add("-x");
+ " --no-playlist" cmd.add("-f=" + formatCodes + "worstaudio/bestaudio/worst/best");
+ " -o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s " cmd.add("--audio-format=wav");
+ song.getUrl().toString(); cmd.add("--no-playlist");
// cmd.add(" --extractor-args youtube:player_client=android";
Process exec = runCommand(cmd, 300); cmd.add("-N8");
InputStream in = exec.getInputStream(); if (streamOutput)
String output = new String(in.readAllBytes(), Charset.defaultCharset()); {
String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset()); cmd.add("--downloader=ffmpeg"); // download using FFMpeg
System.out.println(output); cmd.add("--downloader-args=ffmpeg:-f wav -c:a pcm_s16le"); // tell FFMpeg to convert to wav
Matcher matcher = DESTINATION_PATTERN.matcher(output); cmd.add("-o"); // output to stdout
if (matcher.find()) cmd.add("-");
song.setLocation(new File(matcher.group(1))); } else
else if (exec.exitValue() != 0) {
throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error); cmd.add("-o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s");
// return true; }
cmd.add(request.getUrl().toString());
if (task.getDestination() != null)
task.getDestination().accept(song); Process exec = runCommand(cmd, streamOutput ? 0 : DOWNLOAD_TIMEOUT);
downloadQueue.remove(task);
messageHandler.accept(task.request, null); if (streamOutput)
{
Scanner sc = new Scanner(exec.getErrorStream());
while (sc.hasNextLine())
{
String line = sc.nextLine();
System.out.println(line);
Matcher streamMatcher = STREAM_PATTERN.matcher(line);
if (streamMatcher.find())
{
break;
}
}
if (!exec.isAlive() && exec.exitValue() != 0)
throw new RuntimeException("yt-dlp failed with error code " + exec.exitValue());
Track track = getTrackFromRequest(request, 0);
BufferedInputStream inBuf = new BufferedInputStream(exec.getInputStream());
inBuf.mark(128);
final byte[] headerBytes = inBuf.readNBytes(80);
String header = new String(headerBytes);
System.out.println("streaming data header: " + header);
if (!header.startsWith("RIFF"))
throw new RuntimeException("Streaming data has bad header!");
inBuf.reset();
track.setInputStream(inBuf);
promise.complete(track);
track.setProgress(100.0);
messageHandler.accept(request, null);
} else
{
Scanner sc = new Scanner(exec.getInputStream());
while (sc.hasNextLine())
{
String line = sc.nextLine();
System.out.println(line);
Matcher itemMatcher = DOWNLOAD_ITEM_PATTERN.matcher(line);
if (itemMatcher.find())
{
int idx = Integer.parseInt(itemMatcher.group(1)) - 1;
downloadIdx = idx;
}
Matcher progMatcher = PROGRESS_PATTERN.matcher(line);
if (progMatcher.find())
{
getTrackFromRequest(request, downloadIdx).setProgress(Double.parseDouble(progMatcher.group(1)));
messageHandler.accept(request, null);
}
Matcher destMatcher = DESTINATION_PATTERN.matcher(line);
if (destMatcher.find())
{
Track track = getTrackFromRequest(request, downloadIdx);
track.setLocation(new File(destMatcher.group(1)));
// this is currently our criteria for completion; submit the track and move on
promise.complete(track);
track.setProgress(100.0);
messageHandler.accept(request, null);
downloadIdx++;
}
}
boolean exited = exec.waitFor(DOWNLOAD_TIMEOUT, TimeUnit.SECONDS);
if (exited)
{
String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset());
if (exec.exitValue() != 0)
throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error);
} else
{
throw new RuntimeException("youtube-dl failed to exit.");
}
}
messageHandler.accept(request, null);
} catch (Exception ex) } catch (Exception ex)
{ {
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
if (messageHandler != null) if (messageHandler != null)
messageHandler.accept(task.request, ex); messageHandler.accept(request, ex);
downloadQueue.remove(task); //downloadQueue.remove(task);
} }
} }
private Process runCommand(String cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException private Process runCommand(List<String> cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException
{ {
System.out.println("Running command: " + cmd); System.out.println("Running command: " + cmd);
// Process exec = Runtime.getRuntime().exec().split(" ")); Process exec = new ProcessBuilder(cmd).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
Process exec = new ProcessBuilder(cmd.split(" ")).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start(); if (timeoutSecs > 0)
boolean done = exec.waitFor(timeoutSecs, TimeUnit.SECONDS);
if (!done)
{ {
exec.destroyForcibly(); scheduler.schedule(() ->
throw new RuntimeException("Took too long, giving up."); {
if (exec.isAlive())
{
exec.destroyForcibly();
System.err.println("Process " + cmd + " took too long, killing process.");
}
}, timeoutSecs, TimeUnit.SECONDS);
} }
return exec; return exec;
} }
public BiConsumer<SongRequest, Exception> getMessageHandler() public BiConsumer<TrackRequest, Exception> getMessageHandler()
{ {
return messageHandler; return messageHandler;
} }
public void setMessageHandler(BiConsumer<SongRequest, Exception> messageHandler) public void setMessageHandler(BiConsumer<TrackRequest, Exception> messageHandler)
{ {
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
} }
public List<DownloadTask> getDownloadQueue() public List<TrackRequest> getDownloadQueue()
{ {
return downloadQueue; return new ArrayList<>(inputQueue);
} }
public static class DownloadTask
{
private final SongRequest request;
private final Consumer<Song> destination;
public DownloadTask(SongRequest request, Consumer<Song> destination) // public static class DownloadTask
{ // {
this.request = request; //
this.destination = destination; // private final TrackRequest request;
} // private final Consumer<Track> destination;
//
public SongRequest getSong() // public DownloadTask(TrackRequest request, Consumer<Track> destination)
{ // {
return request; // this.request = request;
} // this.destination = destination;
// }
public Consumer<Song> getDestination() //
{ // public TrackRequest getTrack()
return destination; // {
} // return request;
// }
//
// public Consumer<Track> getDestination()
// {
// return destination;
// }
//
// }
public List<TrackRequest> getDownloadingTracks()
{
return downloadingTracks;
} }
} }

@ -7,8 +7,8 @@ package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml; import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlMapping; import com.amihaiemil.eoyaml.YamlMapping;
import java.util.regex.Matcher; import java.util.Objects;
import java.util.regex.Pattern; import javax.json.JsonObject;
/** /**
* *
@ -17,13 +17,14 @@ import java.util.regex.Pattern;
class Format class Format
{ {
private static final Pattern SIZE_PATTERN = Pattern.compile("\\b([0-9]+\\.?[0-9]*)([kkMmGg])i?[bB]\\b");
private static final Pattern BITRATE_PATTERN = Pattern.compile("\\b([0-9]+)k(b(ps?))?\\b");
private final String code; private final String code;
private final String extension; private final String extension;
private final String resolution; private final String resolution;
private final String note; private final String note;
private long size = -1;
private int samplerate = -1;
private boolean audioOnly = false;
public Format(String code, String extension, String resolution, String note) public Format(String code, String extension, String resolution, String note)
{ {
@ -33,6 +34,28 @@ class Format
this.note = note; this.note = note;
} }
/**
* Read format info from a youtube-dl JSON response.
*
* @param object the JSON object from youtube-dl.
* @return a new Format object if the JSON is valid, or null if not.
*/
public static Format fromJSON(JsonObject object)
{
String code = object.getString("format_id");
String ext = object.getString("ext");
// String res = object.getString()
String res = object.getString("format");
String note = "";
Format ret = new Format(code, ext, res, note);
int asr = object.getInt("asr", -1);
int size = object.getInt("filesize", -1);
ret.setSampleRate(asr);
ret.setSize(size);
return ret;
}
public static Format fromYaml(YamlMapping yaml) public static Format fromYaml(YamlMapping yaml)
{ {
Format format = new Format( Format format = new Format(
@ -55,46 +78,44 @@ class Format
public boolean isAudioOnly() public boolean isAudioOnly()
{ {
return resolution.trim().toLowerCase().contains("audio only"); if (audioOnly)
return true;
return resolution.trim().toLowerCase().contains("audio only") || note.trim().toLowerCase().contains("tiny");
}
public void setAudioOnly(boolean audioOnly)
{
this.audioOnly = audioOnly;
} }
public long getSize() public long getSize()
{ {
if (size != -1)
return size;
// try to find eg. "1.32MiB" inside note // try to find eg. "1.32MiB" inside note
Matcher matcher = SIZE_PATTERN.matcher(note); long size = Util.parseSize(note);
if (matcher.find())
{ return size;
double value = Double.parseDouble(matcher.group(1)); }
String mag = matcher.group(2).toUpperCase();
long mult = 1; public int getSampleRate()
switch (mag)
{
case "K":
mult = 1024;
break;
case "M":
mult = 1024 * 1024;
break;
case "G":
mult = 1024 * 1024 * 1024;
break;
}
value *= mult;
return (long) value;
}
return -1;
}
public int getBitrate()
{ {
if (samplerate != -1)
return samplerate;
// try to find eg. "51k" inside note // try to find eg. "51k" inside note
Matcher matcher = BITRATE_PATTERN.matcher(note); return Util.parseSampleRate(note);
if (matcher.find()) }
{
return Integer.parseInt(matcher.group(1)) * 1000; public void setSampleRate(int samplerate)
} {
return -1; this.samplerate = samplerate;
}
public void setSize(long size)
{
this.size = size;
} }
public String getCode() public String getCode()
@ -123,4 +144,42 @@ class Format
return "Format{" + "code=" + code + ", extension=" + extension + ", resolution=" + resolution + ", note=" + note + '}'; return "Format{" + "code=" + code + ", extension=" + extension + ", resolution=" + resolution + ", note=" + note + '}';
} }
@Override
public int hashCode()
{
int hash = 7;
hash = 97 * hash + Objects.hashCode(this.extension);
hash = 97 * hash + Objects.hashCode(this.resolution);
hash = 97 * hash + (int) (this.size ^ (this.size >>> 32));
hash = 97 * hash + this.samplerate;
hash = 97 * hash + (this.audioOnly ? 1 : 0);
return hash;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Format other = (Format) obj;
if (this.size != other.size)
return false;
if (this.samplerate != other.samplerate)
return false;
if (this.audioOnly != other.audioOnly)
return false;
if (!Objects.equals(this.code, other.code))
return false;
if (!Objects.equals(this.extension, other.extension))
return false;
if (!Objects.equals(this.resolution, other.resolution))
return false;
return true;
}
} }

@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import java.io.IOException;
import java.net.*;
import javax.net.SocketFactory;
/**
*
* @author jimj316
*/
public class LocalBindSocketFactory extends SocketFactory
{
private InetAddress localAddress;
public InetAddress getLocalAddress()
{
return localAddress;
}
public void setLocalAddress(InetAddress localAddress)
{
this.localAddress = localAddress;
}
@Override
public Socket createSocket() throws IOException
{
return new LocalBoundSocket();
// throw new RuntimeException("Trying to create an empty socket!");
}
private InetSocketAddress findFreeSocketAddress()
{
return new InetSocketAddress(localAddress, findFreePort(localAddress));
}
@Override
public Socket createSocket(String remoteAddr, int remotePort) throws IOException, UnknownHostException
{
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress));
}
@Override
public Socket createSocket(String remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException, UnknownHostException
{
return createSocket(InetAddress.getByName(remoteAddr), remotePort, localAddress, localPort);
}
@Override
public Socket createSocket(InetAddress remoteAddr, int remotePort) throws IOException
{
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress));
}
@Override
public Socket createSocket(InetAddress remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException
{
System.out.println("Requested socket; " + localAddress + ":" + localPort + " -> " + remoteAddr + ":" + remotePort);
Socket socket = new Socket(remoteAddr, remotePort, localAddress, localPort);
System.out.println("Returned socket; " + socket.getLocalSocketAddress() + ":" + socket.getLocalPort() + " -> " + socket.getRemoteSocketAddress() + ":" + socket.getPort());
return socket;
}
private static int findFreePort(InetAddress localAddr)
{
int port = 0;
// For ServerSocket port number 0 means that the port number is automatically allocated.
try (ServerSocket socket = new ServerSocket(0, 0, localAddr))
{
// Disable timeout and reuse address after closing the socket.
socket.setReuseAddress(true);
port = socket.getLocalPort();
} catch (IOException ignored)
{
}
if (port > 0)
{
return port;
}
throw new RuntimeException("Could not find a free port");
}
private class LocalBoundSocket extends Socket
{
@Override
public void bind(SocketAddress bindpoint) throws IOException
{
InetSocketAddress localAddress = findFreeSocketAddress();
System.err.println("LocalBoundSocket NOT binding to " + bindpoint + ", using " + localAddress + " instead!");
super.bind(localAddress);
}
}
}

@ -1,417 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlMapping;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.login.LoginException;
import moe.nekojimi.chords.commands.*;
import moe.nekojimi.musicsearcher.providers.MetaSearcher;
import moe.nekojimi.musicsearcher.providers.Searcher;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.managers.AudioManager;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.Compression;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
/**
*
* @author jimj316
*/
public final class Main extends ListenerAdapter
{
private final File dataDirectory;
private final File playlistsDirectory;
private MusicHandler musicHandler;
private final Downloader downloader;
private final Searcher searcher;
private final QueueManager queueManager;
private JDA jda;
private final Map<String, Command> commands = new HashMap<>();
private final Command helpCommand;
private PlayCommand playCommand;
private VoiceChannel currentVoiceChannel = null;
private int trackNumber = 1;
private final Map<String, Playlist> playlists = new HashMap<>();
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws LoginException, IOException
{
// We only need 2 gateway intents enabled for this example:
EnumSet<GatewayIntent> intents = EnumSet.of(
// We need messages in guilds to accept commands from users
GatewayIntent.GUILD_MESSAGES,
// We need voice states to connect to the voice channel
GatewayIntent.GUILD_VOICE_STATES,
GatewayIntent.GUILD_MEMBERS
);
JDABuilder builder = JDABuilder.createDefault(args[0], intents);
// Disable parts of the cache
builder.disableCache(CacheFlag.MEMBER_OVERRIDES);
// Enable the bulk delete event
builder.setBulkDeleteSplittingEnabled(false);
// Disable compression (not recommended)
builder.setCompression(Compression.NONE);
// Set activity (like "playing Something")
builder.setActivity(Activity.playing("music!"));
final Main listener = new Main();
builder.addEventListeners(listener);
builder.setAutoReconnect(true);
JDA jda = builder.build();
listener.setJda(jda);
}
private final Consumer<Song> nowPlayingConsumer = (Song song) ->
{
if (song != null)
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, song.toString()));
else
jda.getPresence().setActivity(null);
};
private final BiConsumer<SongRequest, Exception> downloaderMessageHandler = (SongRequest request, Exception ex) ->
{
Song song = request.getSong();
// TextChannel channel = jda.getTextChannelById(song.getRequestedIn());
// String bracketNo = "[" + song.getNumber() + "] ";
if (ex == null)
if (song.getLocation() != null)
{
request.respond("Finished downloading " + song + ", added to queue!");
log("DOWN", "Downloaded " + song);
} else
{
Format format = song.getBestFormat();
String formatDetails = "";
if (format != null)
{
final int bitrate = format.getBitrate() / 1000;
final long size = format.getSize();
String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB";
String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k";
formatDetails = " (" + bitFmt + ", " + sizeFmt + ")";
}
request.respond("Now downloading " + song + formatDetails + " ...");
log("DOWN", "Downloading " + song + "...");
}
else
{
request.respond("Failed to download " + song + "! Reason: " + ex.getMessage());
log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage());
}
};
public Main() throws IOException
{
log("INFO", "Starting up...");
// init dirs
dataDirectory = new File(System.getProperty("user.dir"));
playlistsDirectory = initDirectory(dataDirectory, "playlists");
// init downloader
downloader = new Downloader();
downloader.setMessageHandler(downloaderMessageHandler);
// init searcher
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
// init queue manager
queueManager = new QueueManager();
// init commands
addCommand(new JoinCommand(this));
addCommand(new LeaveCommand(this));
playCommand = new PlayCommand(this);
addCommand(playCommand);
addCommand(new QueueCommand(this));
addCommand(new RemoveCommand(this));
addCommand(new RestartCommand(this));
addCommand(new SkipCommand(this));
helpCommand = new HelpCommand(this);
addCommand(helpCommand);
// load playlists
loadPlaylists();
log("INFO", "Started OK!");
}
private void addCommand(Command command)
{
commands.put(command.getKeyword(), command);
}
public void setJda(JDA jda)
{
this.jda = jda;
}
@Override
public void onGuildVoiceLeave(GuildVoiceLeaveEvent event)
{
if (this.currentVoiceChannel == null)
return;
final VoiceChannel channelLeft = event.getChannelLeft();
if (channelLeft.getMembers().isEmpty())
if (channelLeft == currentVoiceChannel)
disconnect();
}
@Override
public void onGuildMessageReceived(GuildMessageReceivedEvent event)
{
Message message = event.getMessage();
User author = message.getAuthor();
String content = message.getContentRaw();
Guild guild = event.getGuild();
// Ignore message if it's not in a music channel
if (!event.getChannel().getName().contains("music"))
return;
// Ignore message if bot
if (author.isBot())
return;
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content);
try
{
URL parseURL = new URL(content.trim());
playCommand.call(event, List.of(parseURL.toExternalForm()));
} catch (MalformedURLException ex)
{
// not a URL, then
}
try
{
String[] split = content.split("\\s+", 2);
String cmd = split[0].toLowerCase();
if (!cmd.startsWith("!"))
return; // doesn't start with prefix char
cmd = cmd.substring(1); // strip prefix char
String arg = "";
if (split.length > 1)
arg = split[1];
if (commands.containsKey(cmd))
{
Command command = commands.get(cmd);
command.call(event, List.of(arg));
} else
{
helpCommand.call(event, List.of(arg));
}
} catch (Exception ex)
{
event.getChannel().sendMessage("Error in command! " + ex.getMessage()).queue();
log("UERR", "Command error:" + ex.getMessage());
}
}
public Song queueDownload(SongRequest request)
{
Song song;
if (request.getUrl() != null)
{
song = new Song(request.getUrl());
} else
{
// interpret search result
throw new UnsupportedOperationException("Not supported yet.");
}
if (request.getRequestMessage() != null)
{
song.setRequestedBy(request.getRequestMessage().getAuthor().getName());
song.setRequestedIn(request.getRequestMessage().getChannel().getId());
}
song.setNumber(trackNumber);
trackNumber++;
request.setSong(song);
downloader.accept(new Downloader.DownloadTask(request, queueManager));
request.respond("Request pending...");
return song;
}
// public Song queueDownload(final URL url, GuildMessageReceivedEvent event)
// {
// Song song = new Song(url);
// song.setRequestedBy(event.getAuthor().getName());
// song.setRequestedIn(event.getChannel().getId());
// song.setNumber(trackNumber);
// trackNumber++;
// downloader.accept(new Downloader.DownloadTask(song, musicHandler));
// return song;
// }
//
// public Song queueDownload(Result res, GuildMessageReceivedEvent event)
// {
// Song song = queueDownload(res.getLink(), event);
// song.setArtist(res.getArtist());
// song.setTitle(res.getTitle());
// song.setNumber(trackNumber);
// return song;
// }
public void setStatus(Song nowPlaying)
{
jda.getPresence().setActivity(Activity.listening(nowPlaying.toString()));
}
public void clearStatus()
{
jda.getPresence().setActivity(null);
}
/**
* Connect to requested channel and start echo handler
*
* @param channel
* The channel to connect to
*/
public void connectTo(VoiceChannel channel)
{
Guild guild = channel.getGuild();
// Get an audio manager for this guild, this will be created upon first use for each guild
AudioManager audioManager = guild.getAudioManager();
musicHandler = new MusicHandler();
queueManager.setHandler(musicHandler);
// Create our Send/Receive handler for the audio connection
// EchoHandler handler = new EchoHandler();
// The order of the following instructions does not matter!
// Set the sending handler to our echo system
audioManager.setSendingHandler(musicHandler);
// Set the receiving handler to the same echo system, otherwise we can't echo anything
// audioManager.setReceivingHandler(handler);
// Connect to the voice channel
audioManager.openAudioConnection(channel);
currentVoiceChannel = channel;
musicHandler.setNowPlayingConsumer(nowPlayingConsumer);
}
public void disconnect()
{
if (currentVoiceChannel != null)
{
Guild guild = currentVoiceChannel.getGuild();
AudioManager audioManager = guild.getAudioManager();
audioManager.setSendingHandler(null);
audioManager.closeAudioConnection();
musicHandler = null;
currentVoiceChannel = null;
}
}
public void log(String type, String message)
{
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message);
}
public File initDirectory(File parent, String name) throws IOException
{
File ret = new File(parent, name);
if (!ret.exists())
Files.createDirectories(ret.toPath());
if (!ret.canRead())
throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!");
if (!ret.canWrite())
throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!");
return ret;
}
private void loadPlaylists()
{
File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml"));
for (File file : files)
{
try
{
YamlMapping map = Yaml.createYamlInput(file).readYamlMapping();
Playlist playlist = Playlist.fromYaml(map);
playlists.put(playlist.getName(), playlist);
} catch (IOException ex)
{
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public MusicHandler getMusicHandler()
{
return musicHandler;
}
public Downloader getDownloader()
{
return downloader;
}
public Searcher getSearcher()
{
return searcher;
}
public JDA getJda()
{
return jda;
}
public VoiceChannel getCurrentVoiceChannel()
{
return currentVoiceChannel;
}
public QueueManager getQueueManager()
{
return queueManager;
}
public int getTrackNumber()
{
return trackNumber;
}
}

@ -7,54 +7,50 @@ package moe.nekojimi.chords;
import java.io.*; import java.io.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import net.dv8tion.jda.api.audio.AudioSendHandler; import net.dv8tion.jda.api.audio.AudioSendHandler;
import org.apache.commons.io.input.buffer.CircularByteBuffer;
/** /**
* *
* @author jimj316 * @author jimj316
*/ */
public class MusicHandler implements AudioSendHandler, Closeable public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track>
{ {
// private QueueThing<?, Track> queueManager;
private QueueManager queueManager; private QueueManager queueManager;
// private final LinkedList<Song> songQueue = new LinkedList<>(); // private final LinkedList<Track> trackQueue = new LinkedList<>();
// private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>(); // private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); // private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
private boolean shouldPlay = true; private boolean shouldPlay = true;
private int byteCount; private int byteCount;
private boolean arrayErr = false; private boolean arrayErr = false;
private Consumer<Song> nowPlayingConsumer; private Consumer<Track> nowPlayingConsumer;
public void setNowPlayingConsumer(Consumer<Song> nowPlayingConsumer) public void setNowPlayingConsumer(Consumer<Track> nowPlayingConsumer)
{ {
this.nowPlayingConsumer = nowPlayingConsumer; this.nowPlayingConsumer = nowPlayingConsumer;
} }
private Song currentSong; private Track currentTrack;
private TrackPlayer player; // private TrackPlayer player;
private final List<TrackPlayer> playingTracks = new ArrayList<>();
private File debugOutFile;
private BufferedOutputStream debugOut;
public MusicHandler() public MusicHandler()
{ {
try
{ }
debugOutFile = new File("debug.wav");
if (debugOutFile.exists()) @Override
debugOutFile.delete(); public void accept(Track t)
debugOutFile.createNewFile(); {
debugOut = new BufferedOutputStream(new FileOutputStream(debugOutFile)); play(t);
} catch (IOException ex)
{
Logger.getLogger(MusicHandler.class.getName()).log(Level.SEVERE, null, ex);
}
} }
void setQueueManager(QueueManager manager) void setQueueManager(QueueManager manager)
@ -62,66 +58,74 @@ public class MusicHandler implements AudioSendHandler, Closeable
queueManager = manager; queueManager = manager;
} }
public void playNext()
public void playOver(Track track)
{ {
nextSong(true);
}
// public boolean restartSong() }
// {
//// songQueue.addFirst(currentSong);
// currentSong = null;
// return nextSong(true);
// }
private boolean nextSong() public boolean play(Track track)
{ {
return nextSong(false); return play(track, false);
} }
public boolean nextSong(boolean immediate) public boolean play(Track track, boolean immediate)
{ {
if (track == currentTrack)
return false;
if (immediate) if (immediate)
{ {
System.out.println("Immediate next - clearing buffer"); System.out.println("Immediate next - clearing buffer");
audioBuffer.clear(); playingTracks.clear();
} }
try try
{ {
if (currentSong != null) if (currentTrack != null)
{ {
if (!currentSong.isKept()) if (!currentTrack.isKept())
currentSong.delete(); currentTrack.delete();
currentSong = null; currentTrack = null;
} }
currentSong = queueManager.nextSongNeeded(); currentTrack = track;
if (nowPlayingConsumer != null) if (nowPlayingConsumer != null)
nowPlayingConsumer.accept(currentSong); nowPlayingConsumer.accept(currentTrack);
if (currentSong == null) if (currentTrack == null)
{ {
System.out.println("End of queue.");
debugOut.flush();
return false; return false;
} }
System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath()); // System.out.println("Playing track " + currentTrack.getLocation().getAbsolutePath());
arrayErr = false; arrayErr = false;
byteCount = 3840; byteCount = 3840;
player = new TrackPlayer(currentSong); TrackPlayer player = new TrackPlayer(currentTrack);
playingTracks.add(player);
// System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes()); // System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes());
return true; return true;
} catch (UnsupportedAudioFileException | IOException ex) } catch (UnsupportedAudioFileException | IOException ex)
{ {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex);
currentTrack = null;
requestTrack();
} finally } finally
{ {
} }
return false; return false;
} }
public boolean requestTrack()
{
if (isPlaying())
System.out.println("How did we get here?");
System.out.println("MusicHandler requesting track...");
List<QueueThing.Promise<Track, Track>> request = queueManager.request(1, this);
// Queuemanager will syncronously attempt to call play()
return !playingTracks.isEmpty();
}
public boolean isPlaying() public boolean isPlaying()
{ {
return player != null; return !playingTracks.isEmpty();
} }
public boolean isShouldPlay() public boolean isShouldPlay()
@ -132,14 +136,19 @@ public class MusicHandler implements AudioSendHandler, Closeable
public void setShouldPlay(boolean shouldPlay) public void setShouldPlay(boolean shouldPlay)
{ {
if (!this.shouldPlay && shouldPlay) if (!this.shouldPlay && shouldPlay)
nextSong(); requestTrack();
this.shouldPlay = shouldPlay; this.shouldPlay = shouldPlay;
} }
@Override @Override
public boolean canProvide() public boolean canProvide()
{ {
return player != null && player.has(1); if (playingTracks.isEmpty())
return false;
for (TrackPlayer player : playingTracks)
if (player.has(1))
return true;
return false;
// If we have something in our buffer we can provide it to the send system // If we have something in our buffer we can provide it to the send system
// return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay; // return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay;
} }
@ -148,27 +157,35 @@ public class MusicHandler implements AudioSendHandler, Closeable
public ByteBuffer provide20MsAudio() public ByteBuffer provide20MsAudio()
{ {
ByteBuffer ret = ByteBuffer.allocate(byteCount); ByteBuffer ret = ByteBuffer.allocate(byteCount);
while (ret.position() < byteCount && player != null) while (ret.position() < byteCount && !playingTracks.isEmpty())
{ {
// System.out.println("Position: " + ret.position() + " Remaining: " + ret.remaining()); boolean outOfInput = true;
try List<ByteBuffer> mixes = new ArrayList<>();
List<TrackPlayer> emptyPlayers = new ArrayList<>();
for (TrackPlayer player : playingTracks)
{ {
ByteBuffer read = player.read(ret.remaining()); try
// System.out.println("SAMPLES from player: " + Util.printSamples(read)); {
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read); ByteBuffer read = player.read(ret.remaining());
if (ret.limit() + read.position() >= byteCount)
// System.out.println("Read: " + read.remaining()); outOfInput = false;
ret.put(read); mixes.add(read);
} catch (TrackPlayer.OutOfInputException | IOException ex) // ret.put(read);
} catch (TrackPlayer.OutOfInputException | IOException ex)
{
// System.out.println("Track player " + player + " stopped giving input: " + ex.getMessage());
emptyPlayers.add(player);
// System.out.println("Track ended, starting next.");
// outOfInput = true;
}
}
playingTracks.removeAll(emptyPlayers);
ret.put(mixBuffers(mixes));
if (outOfInput)
{ {
// System.out.println("Track ended, starting next."); boolean foundNext = requestTrack();
boolean foundNext = nextSong();
if (!foundNext) if (!foundNext)
{
// System.out.println("Out of tracks!");
break; break;
}
} }
} }
@ -179,59 +196,9 @@ public class MusicHandler implements AudioSendHandler, Closeable
} }
// private void fillBuffer(boolean canSkip) public Track getCurrentTrack()
// { {
// // use what we have in our buffer to send audio as PCM return currentTrack;
// while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
// if (!readData())
// if (!canSkip || !nextSong())
// break;
// }
//
// private boolean readData()
// {
// if (din == null)
// return false;
// try
// {
// // if (din.available() == 0)
// // return false;
// int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
// int space = audioBuffer.getSpace();
// if (din.available() > 0 && din.available() < bytesToRead)
// bytesToRead = din.available();
// if (bytesToRead > space)
// bytesToRead = space;
// if (bytesToRead == 0)
// return false;
// byte[] bytes = new byte[bytesToRead];
// // byte[] bytes = din.readNBytes(bytesToRead);
// int read = din.read(bytes);
//// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
// if (read < 0)
// return false;
//// queue.add(bytes);
//
// audioBuffer.add(bytes, 0, read);
// } catch (IOException ex)
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// } catch (ArrayIndexOutOfBoundsException ex)
// {
// if (!arrayErr)
// arrayErr = true;
// else
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// }
// }
// return true;
// }
public Song getCurrentSong()
{
return currentSong;
} }
@Override @Override
@ -245,4 +212,48 @@ public class MusicHandler implements AudioSendHandler, Closeable
{ {
} }
private ByteBuffer mixBuffers(List<ByteBuffer> mixes)
{
// System.out.println("Mixing " + mixes.size() + " buffers");
if (mixes.size() == 1)
return mixes.get(0);
int maxSize = 0;
for (ByteBuffer buf : mixes)
{
if (buf.limit() > maxSize)
maxSize = buf.position();
}
ByteBuffer ret = ByteBuffer.allocate(maxSize);
for (int i = 0; i < ret.limit(); i++)
{
int byteTotal = 0;
int mixCount = 0;
for (ByteBuffer buf : mixes)
{
if (i < buf.limit())
{
byteTotal += buf.get(i);
mixCount++;
}
}
double avg = ((double) byteTotal) / mixCount;
byte byteVal = (byte) Math.round(avg);
ret.put(byteVal);
}
ret.rewind();
return ret;
}
public boolean skipTrack()
{
if (!isPlaying())
return false;
playingTracks.clear();
currentTrack = null;
requestTrack();
return true;
}
} }

@ -30,14 +30,14 @@ import java.util.logging.Logger;
* *
* @author jimj316 * @author jimj316
*/ */
public class Playlist implements Consumer<Song> public class Playlist implements Consumer<Track>
{ {
private static final int SHUFFLE_DONT_REPEAT_LAST = 3; private static final int SHUFFLE_DONT_REPEAT_LAST = 3;
private final String name; private final String name;
private final List<Song> songs = new ArrayList<>(); private final List<Track> tracks = new ArrayList<>();
private final LinkedList<Song> playHistory = new LinkedList<>(); private final LinkedList<Track> playHistory = new LinkedList<>();
public Playlist(String name) public Playlist(String name)
{ {
@ -46,25 +46,25 @@ public class Playlist implements Consumer<Song>
public YamlMapping toYaml() public YamlMapping toYaml()
{ {
YamlSequenceBuilder songList = Yaml.createYamlSequenceBuilder(); YamlSequenceBuilder trackList = Yaml.createYamlSequenceBuilder();
for (Song song : songs) for (Track track : tracks)
songList = songList.add(song.toYaml()); trackList = trackList.add(track.toYaml());
return Yaml.createYamlMappingBuilder() return Yaml.createYamlMappingBuilder()
.add("name", name) .add("name", name)
.add("songs", songList.build()) .add("tracks", trackList.build())
.build(); .build();
} }
public static Playlist fromYaml(YamlMapping yaml) public static Playlist fromYaml(YamlMapping yaml)
{ {
Playlist ret = new Playlist(yaml.string("name")); Playlist ret = new Playlist(yaml.string("name"));
YamlSequence songList = yaml.value("songs").asSequence(); YamlSequence trackList = yaml.value("tracks").asSequence();
for (int i = 0; i < songList.size(); i++) for (int i = 0; i < trackList.size(); i++)
{ {
try try
{ {
ret.addSong(Song.fromYaml(songList.yamlMapping(i))); ret.addTrack(Track.fromYaml(trackList.yamlMapping(i)));
} catch (MalformedURLException ex) } catch (MalformedURLException ex)
{ {
Logger.getLogger(Playlist.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(Playlist.class.getName()).log(Level.SEVERE, null, ex);
@ -73,10 +73,10 @@ public class Playlist implements Consumer<Song>
return ret; return ret;
} }
public void addSong(Song song) public void addTrack(Track track)
{ {
song.setKept(true); track.setKept(true);
songs.add(song); tracks.add(track);
} }
public String getName() public String getName()
@ -84,17 +84,17 @@ public class Playlist implements Consumer<Song>
return name; return name;
} }
public List<Song> getSongs() public List<Track> getTracks()
{ {
return songs; return tracks;
} }
public Song getNextSong() public Track getNextTrack()
{ {
Song ret; Track ret;
// copy the song list // copy the track list
List<Song> toShuffle = new LinkedList<>(songs); List<Track> toShuffle = new LinkedList<>(tracks);
// remove play history from candidates, latest first, unless we'd have less than 2 options // remove play history from candidates, latest first, unless we'd have less than 2 options
for (int i = playHistory.size() - 1; i >= 0; i--) for (int i = playHistory.size() - 1; i >= 0; i--)
@ -114,9 +114,9 @@ public class Playlist implements Consumer<Song>
} }
@Override @Override
public void accept(Song t) public void accept(Track t)
{ {
addSong(t); addTrack(t);
} }
} }

@ -16,7 +16,8 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.util.LinkedList; import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -24,75 +25,122 @@ import java.util.function.Consumer;
* *
* @author jimj316 * @author jimj316
*/ */
public class QueueManager implements Consumer<Song> public class QueueManager extends QueueThing<Track, Track>
{ {
private final Queue<Song> jukeboxQueue;
private final int QUEUE_TARGET_SIZE = 5;
private Track restartingTrack = null;
// private final PriorityQueue<Track> jukeboxQueue = new PriorityQueue<>();
private QueueThing<?, Track> trackSource;
private Playlist playlist; private Playlist playlist;
private MusicHandler handler; private MusicHandler handler;
public QueueManager() public QueueManager()
{ {
jukeboxQueue = new LinkedList<>(); super(new PriorityQueue<Track>());
queueTargetSize = QUEUE_TARGET_SIZE;
// jukeboxQueue = new LinkedList<>();
} }
@Override @Override
public void accept(Song t) protected void notifyNewInput()
{ {
jukeboxQueue.add(t); super.notifyNewInput();
if (!handler.isPlaying()) // if (inputQueue.size() < QUEUE_TARGET_SIZE)
handler.playNext(); // demandInput(QUEUE_TARGET_SIZE - inputQueue.size());
if (handler != null && !handler.isPlaying() || handler.getCurrentTrack() == null)
handler.requestTrack();
} }
/** @Override
* Called by the music handler when the current song has ended, or if protected boolean completePromise(Promise<Track, Track> request)
* playNext is called with nothing playing.
*
* @return the next track to play, or null to stop playing.
*/
public Song nextSongNeeded()
{ {
// if there's anything in the queue, play that first final Track input = request.getInput();
if (!jukeboxQueue.isEmpty()) if (input != null)
{ {
return jukeboxQueue.poll(); request.complete(input);
return true;
} }
// otherwise if there's a playlist, shuffle from that return false;
else if (playlist != null) }
@Override
public List<Promise<Track, Track>> request(int count, Consumer<Track> destination)
{
if (restartingTrack != null)
{ {
return playlist.getNextSong(); Promise ret = new Promise<>(restartingTrack, destination);
restartingTrack = null;
return List.of(ret);
} }
// otherwise stop playing
else List<Promise<Track, Track>> ret = super.request(count, destination);
return null; if (inputQueue.size() < QUEUE_TARGET_SIZE)
demandInput(QUEUE_TARGET_SIZE - inputQueue.size());
return ret;
} }
/**
* Called by the music handler when the current track has ended, or if
* playNext is called with nothing playing.
*
* @return the next track to play, or null to stop playing.
*/
// public Track nextTrackNeeded()
// {
// Track ret;
// // if we're restarting the current track: store, clear, and return it
// if (restartingTrack != null)
// {
// ret = restartingTrack;
// restartingTrack = null;
// }
// // if there's anything in the queue, play that first
// else if (!jukeboxQueue.isEmpty())
// {
// ret = jukeboxQueue.poll();
// }
// // otherwise if there's a playlist, shuffle from that
// else if (playlist != null)
// {
// ret = playlist.getNextTrack();
// }
// // otherwise stop playing
// else
// ret = null;
//
// return ret;
// }
public MusicHandler getHandler() public MusicHandler getHandler()
{ {
return handler; return handler;
} }
public void addSong(Song song) public void addTrack(Track track)
{ {
System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath()); System.out.println("Track added to queue: " + track.getLocation().getAbsolutePath());
jukeboxQueue.add(song); inputQueue.add(track);
} }
public boolean removeSong(int i) public boolean removeTrack(int i)
{ {
try try
{ {
return jukeboxQueue.remove((Song) jukeboxQueue.toArray()[i]); return inputQueue.remove((Track) inputQueue.toArray()[i]);
} catch (ArrayIndexOutOfBoundsException ex) } catch (ArrayIndexOutOfBoundsException ex)
{ {
return false; return false;
} }
} }
public boolean removeSong(Song song) public boolean removeTrack(Track track)
{ {
return jukeboxQueue.remove(song); return inputQueue.remove(track);
} }
public void setHandler(MusicHandler handler) public void setHandler(MusicHandler handler)
@ -101,9 +149,9 @@ public class QueueManager implements Consumer<Song>
handler.setQueueManager(this); handler.setQueueManager(this);
} }
public Queue<Song> getJukeboxQueue() public Queue<Track> getJukeboxQueue()
{ {
return jukeboxQueue; return inputQueue;
} }
public Playlist getPlaylist() public Playlist getPlaylist()
@ -116,8 +164,24 @@ public class QueueManager implements Consumer<Song>
this.playlist = playlist; this.playlist = playlist;
} }
public boolean restartSong() public boolean restartTrack()
{
restartingTrack = handler.getCurrentTrack();
if (restartingTrack != null)
{
handler.requestTrack();
return true;
} else
return false;
}
public QueueThing<?, Track> getTrackSource()
{
return trackSource;
}
public void setTrackSource(QueueThing<?, Track> trackSource)
{ {
throw new UnsupportedOperationException("Not supported yet."); this.trackSource = trackSource;
} }
} }

@ -0,0 +1,211 @@
/*
* Copyright (C) 2023 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.function.Consumer;
/**
*
* @author jimj316
*/
public abstract class QueueThing<I, O> implements Consumer<I>
{
// TODO: better name
protected final Queue<I> inputQueue;
protected final List<QueueThing<?, I>> sources = new ArrayList<>();
protected final List<QueueThing<O, ?>> sinks = new ArrayList<>();
protected final Queue<Promise<I, O>> pendingPromises = new LinkedList<>();
protected int queueTargetSize = 0;
protected QueueThing(Queue<I> inputQueue)
{
this.inputQueue = inputQueue;
}
@Override
public void accept(I t)
{
// FIXME: this causes queue-jumping
// if we've got pending promises, fullfill them, otherwise just put it in the queue and notify the sinks
if (pendingPromises.isEmpty())
{
inputQueue.add(t);
notifyNewInput();
} else
{
Promise<I, O> promise = pendingPromises.poll();
System.out.println(this.getClass().getSimpleName() + " now has " + pendingPromises + " promises.");
promise.setInput(t);
handlePromise(promise);
}
}
/**
* Called to notify downstream modules that there's input upstream -
* consider requesting it to pull it through
*/
protected void notifyNewInput()
{
if (inputQueue.size() < queueTargetSize)
demandInput(queueTargetSize - inputQueue.size());
for (QueueThing<O, ?> sink : sinks)
{
sink.notifyNewInput();
}
}
public List<Promise<I, O>> request(int count, Consumer<O> destination)
{
List<Promise<I, O>> ret = new ArrayList<>();
int demandsForClient = 0;
for (int i = 0; i < count; i++)
{
I input = null;
if (!inputQueue.isEmpty())
{
input = inputQueue.poll();
}
Promise<I, O> promise = new Promise<>(input, destination);
boolean ok = handlePromise(promise);
if (ok)
{
// we got a promise of output so we can tell the client
ret.add(promise);
} else
{
// we need to get more input from sources
demandsForClient++;
}
}
int demandsForMe = queueTargetSize - inputQueue.size();
int demandsTotal = demandsForMe + demandsForClient;
if (demandsTotal > 0)
{
// try to get more input from sources
int sourcePromises = demandInput(demandsForClient);
// each promise of input we get represents a promise of output we can give
for (int i = 0; i < sourcePromises && i < demandsForClient; i++)
{
Promise<I, O> myPromise = new Promise<>(destination);
pendingPromises.add(myPromise);
System.out.println(this.getClass().getSimpleName() + " has made " + pendingPromises + " promises.");
ret.add(myPromise);
}
}
return ret;
}
private boolean handlePromise(Promise<I, O> promise)
{
boolean ok;
ok = completePromise(promise);
return ok;
}
protected abstract boolean completePromise(Promise<I, O> request);
/**
* Requests from sources a certain about of input, to be provided later.
*
* @param count the number of input items to request.
* @return a number Promises of input items. May be more or less than count.
*/
protected int demandInput(int count)
{
int ret = 0;
for (QueueThing<?, I> source : sources)
{
List<Promise<?, I>> promises = (List<Promise<?, I>>) source.request(count - ret, this);
ret += promises.size();
if (ret >= count)
break;
}
return ret;
}
public void addSource(QueueThing<?, I> source)
{
sources.add(source);
source.addSink(this);
}
public void removeSource(QueueThing<?, I> source)
{
sources.remove(source);
source.removeSink(this);
}
private void addSink(QueueThing<O, ?> sink)
{
sinks.add(sink);
}
private void removeSink(QueueThing<O, ?> sink)
{
sinks.remove(sink);
}
public static class Promise<I, O>
{
private I input;
private final Consumer<O> output;
public Promise(I input, Consumer<O> output)
{
this.input = input;
this.output = output;
}
public Promise(Consumer<O> output)
{
this.output = output;
}
public void setInput(I input)
{
this.input = input;
}
public void complete(O out)
{
output.accept(out);
}
public I getInput()
{
return input;
}
}
// protected void submit(O output)
// {
// if (nextStage != null)
// nextStage.accept(output);
// }
}

@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlInput;
import com.amihaiemil.eoyaml.YamlMapping;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
*
* @author jimj316
*/
public class Settings
{
private final YamlMapping mapping;
public Settings(File file) throws FileNotFoundException, IOException
{
YamlInput input = Yaml.createYamlInput(file);
mapping = input.readYamlMapping();
}
public String getDiscordToken()
{
return mapping.string("discord-token");
}
public String getLocalAddr()
{
return mapping.string("local-addr");
}
public String getYtdlCommand()
{
String ret = mapping.string("ytdl-cmd");
if (ret == null)
ret = "/usr/bin/youtube-dl";
return ret;
}
}

@ -10,6 +10,9 @@ import com.amihaiemil.eoyaml.YamlMapping;
import com.amihaiemil.eoyaml.YamlSequence; import com.amihaiemil.eoyaml.YamlSequence;
import com.amihaiemil.eoyaml.YamlSequenceBuilder; import com.amihaiemil.eoyaml.YamlSequenceBuilder;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,22 +22,33 @@ import java.util.List;
* *
* @author jimj316 * @author jimj316
*/ */
public class Song public class Track implements Comparable<Track>
{ {
private String title; private String title;
private String artist; private String artist;
private final URL url; private final URL url;
private File location = null; private File location = null;
private InputStream inputStream = null;
private int number; private int number;
private List<Format> formats = new ArrayList<>(); private List<Format> formats = new ArrayList<>();
private String requestedBy;
private String requestedIn;
private boolean kept = false; private boolean kept = false;
public Song(URL url) private double progress = -1;
private double eta = -1;
private final TrackRequest request;
public Track(URL url, TrackRequest request)
{ {
this.url = url; this.url = url;
this.request = request;
}
public Track(TrackRequest request)
{
this.request = request;
this.url = request.getUrl();
} }
public YamlMapping toYaml() public YamlMapping toYaml()
@ -50,30 +64,30 @@ public class Song
.add("location", location.getAbsolutePath()) .add("location", location.getAbsolutePath())
.add("num", Integer.toString(number)) .add("num", Integer.toString(number))
.add("formats", build.build()) .add("formats", build.build())
.add("requestedBy", requestedBy) // .add("requestedBy", requestedBy)
.add("requestedIn", requestedIn) // .add("requestedIn", requestedIn)
.add("kept", Boolean.toString(kept)) .add("kept", Boolean.toString(kept))
.build(); .build();
} }
public static Song fromYaml(YamlMapping map) throws MalformedURLException public static Track fromYaml(YamlMapping map) throws MalformedURLException
{ {
Song song = new Song(new URL(map.string("url"))); Track track = new Track(new URL(map.string("url")), null);
song.setArtist(map.string("artist")); track.setArtist(map.string("artist"));
song.setTitle(map.string("title")); track.setTitle(map.string("title"));
song.setLocation(new File(map.string("location"))); track.setLocation(new File(map.string("location")));
song.setNumber(map.integer("num")); track.setNumber(map.integer("num"));
song.setKept(Boolean.parseBoolean(map.string("kept"))); track.setKept(Boolean.parseBoolean(map.string("kept")));
song.setRequestedBy(map.string("requestedBy")); // track.setRequestedBy(map.string("requestedBy"));
song.setRequestedIn(map.string("requestedIn")); // track.setRequestedIn(map.string("requestedIn"));
List<Format> formats = new ArrayList<>(); List<Format> formats = new ArrayList<>();
YamlSequence formatSeq = map.yamlSequence("formats"); YamlSequence formatSeq = map.yamlSequence("formats");
for (int i = 0; i < formats.size(); i++) for (int i = 0; i < formats.size(); i++)
formats.add(Format.fromYaml(formatSeq.yamlMapping(i))); formats.add(Format.fromYaml(formatSeq.yamlMapping(i)));
song.setFormats(formats); track.setFormats(formats);
return song; return track;
} }
public boolean isDownloaded() public boolean isDownloaded()
@ -126,31 +140,23 @@ public class Song
this.location = location; this.location = location;
} }
void delete() public InputStream getInputStream() throws FileNotFoundException
{ {
if (location != null) if (inputStream == null && location != null)
location.delete(); inputStream = new FileInputStream(location);
location = null; return inputStream;
} }
public String getRequestedBy() public void setInputStream(InputStream inputStream)
{ {
return requestedBy; this.inputStream = inputStream;
} }
public void setRequestedBy(String requestedBy) void delete()
{
this.requestedBy = requestedBy;
}
public String getRequestedIn()
{
return requestedIn;
}
public void setRequestedIn(String requestedIn)
{ {
this.requestedIn = requestedIn; if (location != null)
location.delete();
location = null;
} }
public boolean isKept() public boolean isKept()
@ -163,6 +169,11 @@ public class Song
this.kept = kept; this.kept = kept;
} }
public TrackRequest getRequest()
{
return request;
}
@Override @Override
public String toString() public String toString()
{ {
@ -197,4 +208,30 @@ public class Song
return formats.get(0); return formats.get(0);
} }
public double getProgress()
{
return progress;
}
public void setProgress(double progress)
{
this.progress = progress;
}
public double getEta()
{
return eta;
}
public void setEta(double eta)
{
this.eta = eta;
}
@Override
public int compareTo(Track o)
{
return Integer.compare(number, o.number);
}
} }

@ -5,9 +5,12 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.io.BufferedInputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem; import javax.sound.sampled.AudioSystem;
@ -24,6 +27,8 @@ public class TrackPlayer implements Closeable
private static final int DESIRED_BUFFER_SIZE = 3840 * 500; private static final int DESIRED_BUFFER_SIZE = 3840 * 500;
private static final int MAX_READ_FAILS = 3; private static final int MAX_READ_FAILS = 3;
private static final int RETRY_COUNT = 8;
private static final int RETRY_DELAY = 100;
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
@ -31,10 +36,38 @@ public class TrackPlayer implements Closeable
private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam
public TrackPlayer(Song song) throws UnsupportedAudioFileException, IOException public TrackPlayer(Track track) throws UnsupportedAudioFileException, IOException
{ {
AudioInputStream in = AudioSystem.getAudioInputStream(song.getLocation()); AudioInputStream in = null;
AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT; AudioFormat decodedFormat = null;
int retry = 0;
while (in == null)
{
try
{
in = AudioSystem.getAudioInputStream(new BufferedInputStream(track.getInputStream()));
decodedFormat = AudioSendHandler.INPUT_FORMAT;
break; // it worked!
} catch (Exception ex)
{
retry++;
if (retry < RETRY_COUNT)
{
System.err.println("Open file " + track.getLocation() + " failed because " + ex.getMessage() + " retry " + retry + "...");
try
{
Thread.sleep(((long) Math.pow(2, retry)) * RETRY_DELAY);
} catch (InterruptedException ex1)
{
}
} else
{
throw ex;
}
}
}
input = AudioSystem.getAudioInputStream(decodedFormat, in); input = AudioSystem.getAudioInputStream(decodedFormat, in);
fillBuffer(false); fillBuffer(false);
} }
@ -58,7 +91,7 @@ public class TrackPlayer implements Closeable
// throw new OutOfInputException(); // throw new OutOfInputException();
int toRead = Math.min(length, audioBuffer.getCurrentNumberOfBytes()); int toRead = Math.min(length, audioBuffer.getCurrentNumberOfBytes());
// System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes()); System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes());
if (toRead <= 0) if (toRead <= 0)
throw new OutOfInputException(); throw new OutOfInputException();
@ -136,6 +169,7 @@ public class TrackPlayer implements Closeable
} }
public static class OutOfInputException extends RuntimeException public static class OutOfInputException extends RuntimeException
{ {

@ -0,0 +1,210 @@
/*
* Copyright (C) 2022 jimj316
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.YamlMapping;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import moe.nekojimi.chords.commands.Invocation;
import moe.nekojimi.musicsearcher.Result;
/**
*
* @author jimj316
*/
public class TrackRequest implements Comparable<TrackRequest>
{
private Invocation invocation;
private String query;
private URL url;
private Result result;
private final List<Track> tracks = new ArrayList<>();
private String requestedBy;
private String requestedIn;
private double priority = 1.0;
public YamlMapping toYAML()
{
throw new UnsupportedOperationException("Not supported yet.");
}
public static TrackRequest fromYAML(YamlMapping yaml)
{
TrackRequest ret = new TrackRequest();
return ret;
}
public Result getResult()
{
return result;
}
public void setResult(Result result)
{
this.result = result;
}
public Invocation getInvocation()
{
return invocation;
}
public void setInvocation(Invocation invocation)
{
this.invocation = invocation;
requestedBy = invocation.getRequestMessage().getAuthor().getName();
requestedIn = invocation.getRequestMessage().getChannel().getId();
}
// public Message getRequestMessage()
// {
// return requestMessage;
// }
//
// public void setRequestMessage(Message requestMessage)
// {
// this.requestMessage = requestMessage;
// }
//
// public Message getResponseMessage()
// {
// return responseMessage;
// }
//
// public void setResponseMessage(Message responseMessage)
// {
// this.responseMessage = responseMessage;
// }
public String getQuery()
{
return query;
}
public void setQuery(String query)
{
this.query = query;
}
public URL getUrl()
{
if (url != null)
return url;
else if (result != null)
return (result.getLink());
else
return null;
}
public void setUrl(URL url)
{
this.url = url;
}
public List<Track> getTracks()
{
return tracks;
}
public void addTrack(Track track)
{
tracks.add(track);
}
public void clearTracks()
{
tracks.clear();
}
public String getRequestedBy()
{
return requestedBy;
}
public String getRequestedIn()
{
return requestedIn;
}
public String toString()
{
String trackName;
if (tracks.isEmpty())
trackName = "Request";
else
{
trackName = tracks.get(0).toString();
if (tracks.size() > 1)
trackName += " & " + (tracks.size() - 1) + " more tracks";
}
String requestName = "";
if (!requestedBy.isEmpty())
requestName = "for " + requestedBy;
return (trackName + " " + requestName).trim();
}
@Override
public int hashCode()
{
int hash = 7;
hash = 83 * hash + Objects.hashCode(this.url);
return hash;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final TrackRequest other = (TrackRequest) obj;
if (!Objects.equals(this.url, other.url))
return false;
return true;
}
@Override
public int compareTo(TrackRequest o)
{
return -Double.compare(priority, o.priority); // backwards so higher priority comes first
}
public double getPriority()
{
return priority;
}
public void setPriority(double priority)
{
this.priority = priority;
}
}

@ -1,6 +1,8 @@
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/* /*
* Copyright (C) 2022 jimj316 * Copyright (C) 2022 jimj316
@ -25,6 +27,10 @@ import java.nio.ByteBuffer;
*/ */
public class Util public class Util
{ {
private static final Pattern SIZE_PATTERN = Pattern.compile("\\b([0-9]+\\.?[0-9]*)([kkMmGg])i?[bB]\\b");
private static final Pattern SAMPLE_RATE_PATTERN = Pattern.compile("\\b([0-9]+)k(b(ps?))?\\b");
public static String printSamples(ByteBuffer buf) public static String printSamples(ByteBuffer buf)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@ -34,4 +40,56 @@ public class Util
} }
return sb.toString(); return sb.toString();
} }
public static int parseSampleRate(String input)
{
Matcher matcher = SAMPLE_RATE_PATTERN.matcher(input);
if (matcher.find())
{
return Integer.parseInt(matcher.group(1)) * 1000;
}
return -1;
}
public static long parseSize(String note)
{
Matcher matcher = SIZE_PATTERN.matcher(note);
if (matcher.find())
{
double value = Double.parseDouble(matcher.group(1));
String mag = matcher.group(2).toUpperCase();
long mult = 1;
switch (mag)
{
case "K":
mult = 1024;
break;
case "M":
mult = 1024 * 1024;
break;
case "G":
mult = 1024 * 1024 * 1024;
break;
}
value *= mult;
return (long) value;
}
return -1L;
}
public static String formatProgressBar(double progressPercent, String[] symbols, int barLength)
{
String ret = "";
double filledSegments = (progressPercent / 100.0) * barLength;
for (int i = 0; i < barLength; i++)
{
double thisSegment = Math.min(1, Math.max(0, filledSegments - i));
int symbolIdx = (int) Math.floor(thisSegment * symbols.length);
String symbol = symbols[symbolIdx];
ret += symbol;
}
return ret;
}
} }

@ -5,9 +5,7 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
/** /**
* *
@ -16,26 +14,35 @@ import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public abstract class Command public abstract class Command
{ {
protected final Main bot; protected final Chords bot;
protected final String keyword; protected final String keyword;
protected String documentation;
public Command(Main bot, String keyword) public Command(Chords bot, String keyword)
{ {
this.bot = bot; this.bot = bot;
this.keyword = keyword; this.keyword = keyword;
} }
public abstract void call(GuildMessageReceivedEvent event, List<String> arg); public abstract void call(Invocation invocation);
public String getKeyword() public String getKeyword()
{ {
return keyword; return keyword;
} }
public String getDocumentation() public String argumentDescription()
{ {
return documentation; return ""; // most commands take no arguments
}
public String synopsis()
{
throw new UnsupportedOperationException("Not supported yet.");
}
//
public String help()
{
throw new UnsupportedOperationException("Not supported yet.");
} }
} }

@ -16,35 +16,36 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import java.util.Map;
import moe.nekojimi.chords.Main; import java.util.Map.Entry;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import moe.nekojimi.chords.Chords;
public class HelpCommand extends Command public class HelpCommand extends Command
{ {
public HelpCommand(Main bot) public HelpCommand(Chords bot)
{ {
super(bot, "help"); super(bot, "help");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
String help = "Commands available:\n" String help = "Commands available:\n"
+ "!join <Channel> - Joins a voice channel\n" + "!join <Channel> - Joins a voice channel\n"
+ "!leave - Leaves the current voice channel\n" + "!leave - Leaves the current voice channel\n"
+ "!play <URL> - Downloads the track at that URL and adds it to the queue.\n" + "!play <URL> - Downloads the track at that URL and adds it to the queue.\n"
+ "!play <Search> - Searches for a track and displays a selection menu.\n" + "!play <Search> - Searches for a track and displays a selection menu.\n"
+ "!queue - Show the songs currently playing and the current queue.\n" + "!queue - Show the tracks currently playing and the current queue.\n"
+ "!remove <Index> - Remove the song at position <Index> from the queue.\n" + "!remove <Index> - Remove the track at position <Index> from the queue.\n"
+ "!skip - Skip the current song and play the next one.\n" + "!skip - Skip the current track and play the next one.\n"
+ "!restart - Try playing the current song again in case it goes wrong.\n"; + "!restart - Try playing the current track again in case it goes wrong.\n";
// for (String key: commands.keySet()) final Map<String, Command> commands = bot.getCommands();
// { for (Entry<String, Command> e : commands.entrySet())
// help += "!" + key + ":" {
// } help += "!" + e.getKey() + " ".repeat(10 - e.getKey().length()) + "- " + e.getValue().synopsis();
event.getChannel().sendMessage(help).queue(); }
invocation.respond(help);
} }
} }

@ -14,70 +14,52 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords.commands;
import java.net.URL;
import java.util.List; import java.util.List;
import moe.nekojimi.musicsearcher.Result;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.requests.restaction.MessageAction; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.internal.entities.DataMessage;
/** /**
* *
* @author jimj316 * @author jimj316
*/ */
public class SongRequest public class Invocation
{ {
private Message requestMessage; private Message requestMessage;
private Message responseMessage; private Message responseMessage;
private String query; private final MessageReceivedEvent event;
private URL url; private final List<String> args;
private List<Result> searchResults; public Invocation(MessageReceivedEvent event, List<String> args)
{
this.event = event;
this.args = args;
}
private Result result; public MessageReceivedEvent getEvent()
{
return event;
}
private Song song; public List<String> getArgs()
{
return args;
}
@SuppressWarnings("null") @SuppressWarnings("null")
public void respond(String text) public void respond(String text)
{ {
MessageAction action = null;
if (responseMessage == null) if (responseMessage == null)
{ {
action = requestMessage.reply(text); responseMessage = requestMessage.reply(text).complete();
} else } else
{ {
action = responseMessage.editMessage(text); responseMessage = responseMessage.editMessage(text).complete();
} }
responseMessage = action.complete();
}
public List<Result> getSearchResults()
{
return searchResults;
}
public void setSearchResults(List<Result> searchResults)
{
this.searchResults = searchResults;
}
public Result getResult()
{
return result;
}
public void setResult(Result result)
{
this.result = result;
} }
public Message getRequestMessage() public Message getRequestMessage()
@ -100,34 +82,4 @@ public class SongRequest
this.responseMessage = responseMessage; this.responseMessage = responseMessage;
} }
public String getQuery()
{
return query;
}
public void setQuery(String query)
{
this.query = query;
}
public URL getUrl()
{
return url;
}
public void setUrl(URL url)
{
this.url = url;
}
public Song getSong()
{
return song;
}
public void setSong(Song song)
{
this.song = song;
}
} }

@ -6,23 +6,26 @@
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import java.util.List;
import java.util.Optional; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public class JoinCommand extends Command public class JoinCommand extends Command
{ {
public JoinCommand(Main bot) public JoinCommand(Chords bot)
{ {
super(bot, "join"); super(bot, "join");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> args) public void call(Invocation invocation)
{ {
TextChannel textChannel = event.getChannel(); MessageReceivedEvent event = invocation.getEvent();
List<String> args = invocation.getArgs();
MessageChannel textChannel = event.getChannel();
VoiceChannel channel = null; VoiceChannel channel = null;
if (args.isEmpty() || args.get(0).isBlank()) if (args.isEmpty() || args.get(0).isBlank())
{ {
@ -30,8 +33,8 @@ public class JoinCommand extends Command
if (member != null) if (member != null)
{ {
GuildVoiceState voiceState = member.getVoiceState(); GuildVoiceState voiceState = member.getVoiceState();
if (voiceState != null && voiceState.inVoiceChannel()) if (voiceState != null && voiceState.inAudioChannel())
channel = voiceState.getChannel(); channel = voiceState.getChannel().asVoiceChannel();
} }
if (channel == null) if (channel == null)
{ {
@ -64,40 +67,20 @@ public class JoinCommand extends Command
if (channel == null) // I have no idea what you want mr user if (channel == null) // I have no idea what you want mr user
{ {
onUnknownChannel(textChannel, arg0); // Let the user know about our failure // onUnknownChannel(textChannel, arg0); // Let the user know about our failure
return; throw new RuntimeException("Unable to connect to \"" + channel.getName() + "\", no such channel!");
} }
} }
bot.connectTo(channel); // We found a channel to connect to! bot.connectTo(channel); // We found a channel to connect to!
onConnecting(channel, textChannel); // Let the user know, we were successful! invocation.respond("Connecting to " + channel.getName() + "...");
// onConnecting(channel, textChannel); // Let the user know, we were successful!
} }
/** @Override
* Inform user about successful connection. public String argumentDescription()
*
* @param channel
* The voice channel we connected to
* @param textChannel
* The text channel to send the message in
*/
public void onConnecting(VoiceChannel channel, TextChannel textChannel)
{
textChannel.sendMessage("Connecting to " + channel.getName()).queue(); // never forget to queue()!
}
/**
* The channel to connect to is not known to us.
*
* @param channel
* The message channel (text channel abstraction) to send failure
* information to
* @param comment
* The information of this channel
*/
public void onUnknownChannel(MessageChannel channel, String comment)
{ {
channel.sendMessage("Unable to connect to ``" + comment + "``, no such channel!").queue(); // never forget to queue()! return "<Channel?>";
} }
} }

@ -5,20 +5,18 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class LeaveCommand extends Command public class LeaveCommand extends Command
{ {
public LeaveCommand(Main bot) public LeaveCommand(Chords bot)
{ {
super(bot, "leave"); super(bot, "leave");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
if (bot.getCurrentVoiceChannel() != null) if (bot.getCurrentVoiceChannel() != null)
{ {

@ -21,11 +21,10 @@ import java.net.URL;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import moe.nekojimi.chords.Main; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.SongRequest; import moe.nekojimi.chords.TrackRequest;
import moe.nekojimi.musicsearcher.Query; import moe.nekojimi.musicsearcher.Query;
import moe.nekojimi.musicsearcher.Result; import moe.nekojimi.musicsearcher.Result;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
/** /**
* *
@ -36,43 +35,43 @@ public class PlayCommand extends Command
private static final double SEARCH_SCORE_THRESHOLD_DISPLAY = 0.6; private static final double SEARCH_SCORE_THRESHOLD_DISPLAY = 0.6;
private static final double SEARCH_SCORE_THRESHOLD_AUTOPLAY = 9999; // disable autoplay it sucks private static final double SEARCH_SCORE_THRESHOLD_AUTOPLAY = 9999; // disable autoplay it sucks
// private List<Result> lastSearchResults; private List<Result> lastSearchResults;
public PlayCommand(Main main) public PlayCommand(Chords main)
{ {
super(main, "play"); super(main, "play");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
SongRequest request = new SongRequest(); TrackRequest request = new TrackRequest();
request.setRequestMessage(event.getMessage()); request.setInvocation(invocation);
try try
{ {
final URL url = new URL(arg.get(0)); final URL url = new URL(invocation.getArgs().get(0));
request.setUrl(url); request.setUrl(url);
bot.queueDownload(request); bot.queueDownload(request);
} catch (MalformedURLException mux) } catch (MalformedURLException mux)
{ {
// not a URL, try parsing it as a search result // not a URL, try parsing it as a search result
if (request.getSearchResults() != null && !request.getSearchResults().isEmpty()) if (lastSearchResults != null && !lastSearchResults.isEmpty())
{ {
try try
{ {
int index = Integer.parseInt(arg.get(0)); int index = Integer.parseInt(invocation.getArgs().get(0));
int size = request.getSearchResults().size(); int size = lastSearchResults.size();
if (index >= 1 && index <= size) if (index >= 1 && index <= size)
{ {
Result result = request.getSearchResults().get(index - 1); Result result = lastSearchResults.get(index - 1);
request.setResult(result); request.setResult(result);
bot.queueDownload(request); bot.queueDownload(request);
// event.getChannel().sendMessage("Song removed.").queue(); // event.getChannel().sendMessage("Track removed.").queue();
} else if (size > 1) } else if (size > 1)
event.getChannel().sendMessage("That's not a number between 1 and " + size + "!").queue(); invocation.respond("That's not a number between 1 and " + size + "!");
else if (size == 1) else if (size == 1)
event.getChannel().sendMessage("There's only one song and that's not one of them!").queue(); invocation.respond("There's only one track and that's not one of them!");
return; return;
} catch (NumberFormatException nfx) } catch (NumberFormatException nfx)
@ -82,22 +81,21 @@ public class PlayCommand extends Command
} }
// otherwise, try searching // otherwise, try searching
CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(arg.stream().reduce((t, u) -> t + " " + u).get())); CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(invocation.getArgs().stream().reduce((t, u) -> t + " " + u).get()));
event.getChannel().sendMessage("Searching for \"" + arg + "\" ...").queue(); invocation.respond("Searching for \"" + invocation.getArgs() + "\" ...");
search.orTimeout(30, TimeUnit.SECONDS).whenCompleteAsync((List<Result> results, Throwable exec) -> search.orTimeout(30, TimeUnit.SECONDS).whenCompleteAsync((List<Result> results, Throwable exec) ->
{ {
if (exec != null) if (exec != null)
{ {
event.getChannel().sendMessage("Failed to search! Reason: " + exec.getMessage()).queue(); invocation.respond("Failed to search! Reason: " + exec.getMessage());
return; return;
} }
request.setSearchResults(results); lastSearchResults = results;
// lastSearchResults = results;
if (results.isEmpty()) if (results.isEmpty())
{ {
event.getChannel().sendMessage("Found nothing! :(").queue(); invocation.respond("Found nothing! :(");
return; return;
} }
@ -125,10 +123,15 @@ public class PlayCommand extends Command
i++; i++;
} }
resultString += "Type eg. `!play 1` to select"; resultString += "Type eg. `!play 1` to select";
event.getChannel().sendMessage(resultString).queue(); invocation.respond(resultString);
}); });
// event.getChannel().sendMessage("That's not a valid URL you idiot! " + ex.getMessage()).queue(); // event.getChannel().sendMessage("That's not a valid URL you idiot! " + ex.getMessage()).queue();
} }
} }
@Override
public String argumentDescription()
{
return "<URL / Search>";
}
} }

@ -18,57 +18,69 @@ package moe.nekojimi.chords.commands;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
import moe.nekojimi.chords.Downloader; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main; import moe.nekojimi.chords.Track;
import moe.nekojimi.chords.Song; import moe.nekojimi.chords.TrackRequest;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class QueueCommand extends Command public class QueueCommand extends Command
{ {
public QueueCommand(Main bot) public QueueCommand(Chords bot)
{ {
super(bot, "queue"); super(bot, "queue");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
String message = ">>> "; String message = ">>> ";
int i = 1; int i = 1;
final Song currentSong = bot.getMusicHandler().getCurrentSong(); Track currentTrack = null;
if (currentSong != null) if (bot.getMusicHandler() != null)
message += ":notes: **Now playing: " + currentSong + "**\n"; currentTrack = bot.getMusicHandler().getCurrentTrack();
if (currentTrack != null)
message += ":notes: **Now playing: " + currentTrack + "**\n";
else else
message += ":mute: **Not playing anything right now.**\n"; message += ":mute: **Not playing anything right now.**\n";
final Queue<Song> songQueue = bot.getQueueManager().getJukeboxQueue(); final Queue<Track> trackQueue = bot.getQueueManager().getJukeboxQueue();
if (!songQueue.isEmpty()) if (!trackQueue.isEmpty())
{ {
message += "__Ready to play:__\n"; message += "__Ready to play:__\n";
for (Song song : songQueue) for (Track track : trackQueue)
{ {
message += ":bread: **" + (i) + ":** " + song + "\n"; message += ":bread: **" + i + ":** " + track + "\n";
i++; i++;
} }
} }
final List<Downloader.DownloadTask> downloadQueue = bot.getDownloader().getDownloadQueue(); final List<TrackRequest> downloading = bot.getDownloader().getDownloadingTracks();
if (!downloadQueue.isEmpty()) if (!downloading.isEmpty())
{ {
message += "__Downloading:__\n"; message += "__Downloading:__\n";
for (Downloader.DownloadTask task : downloadQueue) for (TrackRequest request : downloading)
{
message += ":satellite: **" + (i) + ":** " + request + "\n";
i++;
}
}
final List<TrackRequest> downloadQueue = bot.getDownloader().getDownloadQueue();
if (!downloadQueue.isEmpty())
{
message += "__In queue for download:__\n";
for (TrackRequest request : downloadQueue)
{ {
message += ":inbox_tray: **" + (i) + ":** " + task.getSong() + "\n"; message += ":inbox_tray: **" + (i) + ":** " + request + "\n";
i++; i++;
} }
} }
if (downloadQueue.isEmpty() && songQueue.isEmpty()) if (downloading.isEmpty() && trackQueue.isEmpty() && downloadQueue.isEmpty())
message += ":mailbox_with_no_mail: The track queue is empty."; message += ":mailbox_with_no_mail: The track queue is empty.";
// :inbox_tray: // :inbox_tray:
event.getChannel().sendMessage(message).queue(); invocation.respond(message);
} }
} }

@ -16,36 +16,34 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class RemoveCommand extends Command public class RemoveCommand extends Command
{ {
public RemoveCommand(Main bot) public RemoveCommand(Chords bot)
{ {
super(bot, "remove"); super(bot, "remove");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
try try
{ {
int i = Integer.parseInt(arg.get(0)); int i = Integer.parseInt(invocation.getArgs().get(0));
boolean removed = bot.getQueueManager().removeSong(i - 1); boolean removed = bot.getQueueManager().removeTrack(i - 1);
final int size = bot.getQueueManager().getJukeboxQueue().size(); final int size = bot.getQueueManager().getJukeboxQueue().size();
if (removed) if (removed)
event.getChannel().sendMessage("Song removed.").queue(); invocation.respond("Track removed.");
else if (size > 1) else if (size > 1)
event.getChannel().sendMessage("That's not a number between 1 and " + size + "!").queue(); invocation.respond("That's not a number between 1 and " + size + "!");
else if (size == 1) else if (size == 1)
event.getChannel().sendMessage("There's only one song to remove and that's not one of them!").queue(); invocation.respond("There's only one track to remove and that's not one of them!");
} catch (NumberFormatException ex) } catch (NumberFormatException ex)
{ {
event.getChannel().sendMessage(arg + " isn't a number!").queue(); invocation.respond(invocation.getArgs().get(0) + " isn't a number!");
} }
} }

@ -16,27 +16,25 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class RestartCommand extends Command public class RestartCommand extends Command
{ {
public RestartCommand(Main bot) public RestartCommand(Chords bot)
{ {
super(bot, "restart"); super(bot, "restart");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
// TODO: this needs to clear the current data queue // TODO: this needs to clear the current data queue
boolean ok = bot.getQueueManager().restartSong(); boolean ok = bot.getQueueManager().restartTrack();
if (ok) if (ok)
event.getChannel().sendMessage("Restarted current song!").queue(); invocation.respond("Restarted current track!");
else else
event.getChannel().sendMessage("Cannot restart!").queue(); invocation.respond("Cannot restart!");
} }
} }

@ -16,26 +16,24 @@
*/ */
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; import moe.nekojimi.chords.Chords;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class SkipCommand extends Command public class SkipCommand extends Command
{ {
public SkipCommand(Main bot) public SkipCommand(Chords bot)
{ {
super(bot, "skip"); super(bot, "skip");
} }
@Override @Override
public void call(GuildMessageReceivedEvent event, List<String> arg) public void call(Invocation invocation)
{ {
boolean ok = bot.getMusicHandler().nextSong(true); boolean ok = bot.getMusicHandler().skipTrack();
if (ok) if (ok)
event.getChannel().sendMessage("Skipped to next song!").queue(); invocation.respond("Skipped to next track!");
else else
event.getChannel().sendMessage("There's no more songs!").queue(); invocation.respond("There's no more tracks!");
} }
} }

Loading…
Cancel
Save