Compare commits

..

3 Commits

  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. 366
      src/main/java/moe/nekojimi/chords/Main.java
  15. 204
      src/main/java/moe/nekojimi/chords/MusicHandler.java
  16. 79
      src/main/java/moe/nekojimi/chords/Playlist.java
  17. 138
      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. 106
      src/main/java/moe/nekojimi/chords/Song.java
  21. 88
      src/main/java/moe/nekojimi/chords/SongRequest.java
  22. 177
      src/main/java/moe/nekojimi/chords/TrackPlayer.java
  23. 210
      src/main/java/moe/nekojimi/chords/TrackRequest.java
  24. 58
      src/main/java/moe/nekojimi/chords/Util.java
  25. 25
      src/main/java/moe/nekojimi/chords/commands/Command.java
  26. 29
      src/main/java/moe/nekojimi/chords/commands/HelpCommand.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,9 +55,5 @@ dist/
nbdist/
.nb-gradle/
# Git probably
*.orig
# Secrets!
secrets.yml
settings.yml

@ -1,11 +1,2 @@
# Chords - Yet Another Discord Music Bot
# Chords
![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.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

@ -14,6 +14,5 @@ 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.
-->
<netbeans.hint.license>gpl30</netbeans.hint.license>
<org-netbeans-modules-javascript2-requirejs.enabled>true</org-netbeans-modules-javascript2-requirejs.enabled>
</properties>
</project-shared-configuration>

@ -10,7 +10,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals>
<properties>
<exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.args>-classpath %classpath moe.nekojimi.chords.Main ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.executable>java</exec.executable>
</properties>
</action>
@ -24,7 +24,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals>
<properties>
<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.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.executable>java</exec.executable>
<jpda.listen>true</jpda.listen>
</properties>
@ -39,7 +39,7 @@
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
</goals>
<properties>
<exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.args>-classpath %classpath ${packageClassName} ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
<exec.executable>java</exec.executable>
</properties>
</action>

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

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

@ -1,556 +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 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);
}
}
}

@ -1,52 +0,0 @@
/*
* 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,134 +5,108 @@
*/
package moe.nekojimi.chords;
import com.beust.jcommander.Strings;
import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.*;
import java.util.ArrayList;
import java.util.LinkedList;
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.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import moe.nekojimi.musicsearcher.Result;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import moe.nekojimi.chords.Downloader.DownloadTask;
/**
*
* @author jimj316
*/
public class Downloader extends QueueThing<TrackRequest, Track>
public class Downloader implements Consumer<DownloadTask>
{
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 int BITRATE_TARGET = 64_000;
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 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<TrackRequest> downloadingTracks = new LinkedList<>();
private final List<DownloadTask> downloadQueue = new LinkedList<>();
private final LinkedBlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>();
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue);
private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
// private Consumer<Track> next;
private BiConsumer<TrackRequest, Exception> messageHandler;
private final ThreadPoolExecutor exec = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue);
// private Consumer<Song> next;
private BiConsumer<SongRequest, Exception> messageHandler;
private File downloadDir = null;
private int trackNumber = 1;
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
protected boolean completePromise(Promise<TrackRequest, Track> promise)
public void accept(DownloadTask task)
{
TrackRequest request = promise.getInput();
if (request == null)
return false;
// if already downloaded, just skip
Song song = task.request.getSong();
if (song.isDownloaded())
{
task.getDestination().accept(song);
return;
}
executor.submit(() ->
downloadQueue.add(task);
getInfo(song);
exec.submit(() ->
{
downloadingTracks.add(request);
try
{
getInfo(request);
// boolean streamOutput = request.getTracks().size() == 1;
boolean streamOutput = false;
download(promise, streamOutput);
getFormats(song);
download(task);
} catch (Exception ex)
{
ex.printStackTrace();
}
downloadingTracks.remove(request);
});
return true;
}
private List<Format> sortFormats(Collection<Format> input)
private void chooseFormats(Song song)
{
List<Format> formats = new ArrayList<>(input);
List<Format> formats = song.getFormats();
if (formats.isEmpty())
return;
// System.out.println("Choosing from " + formats.size() + " formats:");
// System.out.println(formats);
formats.sort((Format a, Format b) ->
{
// audio only preferred to video
// System.out.println("sort entered; a=" + a.toString() + " b=" + b.toString());
int comp = 0;
comp = Boolean.compare(a.isAudioOnly(), b.isAudioOnly());
// System.out.println("\tCompared on audio only: " + comp);
if (comp == 0)
{
// known preferred to unknown
if (a.getSampleRate() == b.getSampleRate())
if (a.getBitrate() == b.getBitrate())
comp = 0;
else if (a.getSampleRate() <= 0)
else if (a.getBitrate() <= 0)
comp = 1;
else if (b.getSampleRate() <= 0)
else if (b.getBitrate() <= 0)
comp = -1;
else // closer to the target bitrate is best
{
int aDist = Math.abs(a.getSampleRate() - BITRATE_TARGET);
int bDist = Math.abs(b.getSampleRate() - BITRATE_TARGET);
int aDist = Math.abs(a.getBitrate() - BITRATE_TARGET);
int bDist = Math.abs(b.getBitrate() - BITRATE_TARGET);
comp = Integer.compare(bDist, aDist);
// System.out.println("\tCompared on bitrate distance: " + comp);
}
}
if (comp == 0)
@ -140,85 +114,53 @@ public class Downloader extends QueueThing<TrackRequest, Track>
// known preferred to unknown
if (a.getSize() == b.getSize())
comp = 0;
else if (a.getSize() <= 0)
if (a.getSize() <= 0)
comp = 1;
else if (b.getSize() <= 0)
comp = -1;
else // smaller is better
{
comp = Long.compare(b.getSize(), a.getSize());
// System.out.println("\tCompared on filesize: " + comp);
}
}
// System.out.println("\tOverall: " + comp);
return -comp;
});
return formats;
song.setFormats(formats);
// System.out.println("Sorting done! Formats:" + formats);
}
private void getFormats(Track track)
private void getFormats(Song song)
{
if (!track.getFormats().isEmpty())
return;
//
try
{
// String cmd = + " --skip-download -F " + track.getUrl().toString();
Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "-F", track.getUrl().toString()), FORMAT_TIMEOUT);
String cmd = "/usr/bin/youtube-dl --skip-download -F " + song.getUrl().toString();
Process exec = runCommand(cmd, 5);
InputStream input = exec.getInputStream();
String output = new String(input.readAllBytes(), Charset.defaultCharset());
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());
int i = 0;
while (!list.get(i).contains("Available formats"))
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++)
{
String line = list.get(i);
String[] split = line.split("\\s+", Math.max(4, noteCol - 1));
String[] split = line.split("\\s\\s+", 4);
if (split.length < 4)
continue;
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);
formats.add(new Format(split[0], split[1], split[2], split[3]));
}
track.setFormats(formats);
song.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)
{
@ -227,88 +169,23 @@ public class Downloader extends QueueThing<TrackRequest, Track>
}
}
private List<Track> getInfo(TrackRequest request)
private void getInfo(Song song)
{
List<Track> ret = new ArrayList<>();
try
{
// String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download --print-json " + request.getUrl().toString();
Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "--print-json", request.getUrl().toString()), INFO_TIMEOUT);
String cmd = "/usr/bin/youtube-dl --skip-download --print-json " + song.getUrl().toString();
Process exec = runCommand(cmd, 5);
InputStream input = exec.getInputStream();
// read each line as JSON, turn each into a track object
Scanner sc = new Scanner(input);
while (sc.hasNextLine())
{
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);
}
JsonReader reader = Json.createReader(input);
JsonObject object = reader.readObject();
if (song.getTitle() == null)
song.setTitle(object.getString("title", null));
if (song.getArtist() == null)
song.setArtist(object.getString("uploader", null));
} catch (Exception ex)
{
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
}
return ret;
}
private File getDownloadDir() throws IOException
@ -318,216 +195,101 @@ public class Downloader extends QueueThing<TrackRequest, Track>
return downloadDir;
}
private Track getTrackFromRequest(TrackRequest request, int idx)
private void download(DownloadTask task)
{
// if there's less tracks in the request than expected, fill the array
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);
Song song = task.request.getSong();
chooseFormats(song);
String formatCodes = "";
final List<Format> formats = sortedFormats;
for (int i = 0; i < 5 && i < sortedFormats.size(); i++)
final List<Format> formats = song.getFormats();
for (int i = 0; i < 3 && i < song.getFormats().size(); i++)
formatCodes += formats.get(i).getCode() + "/";
int downloadIdx = 0;
try
{
messageHandler.accept(request, null);
List<String> cmd = new ArrayList<>();
cmd.add(Chords.getSettings().getYtdlCommand());
cmd.add("-x");
cmd.add("-f=" + formatCodes + "worstaudio/bestaudio/worst/best");
cmd.add("--audio-format=wav");
cmd.add("--no-playlist");
// cmd.add(" --extractor-args youtube:player_client=android";
cmd.add("-N8");
if (streamOutput)
{
cmd.add("--downloader=ffmpeg"); // download using FFMpeg
cmd.add("--downloader-args=ffmpeg:-f wav -c:a pcm_s16le"); // tell FFMpeg to convert to wav
cmd.add("-o"); // output to stdout
cmd.add("-");
} else
{
cmd.add("-o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s");
}
cmd.add(request.getUrl().toString());
Process exec = runCommand(cmd, streamOutput ? 0 : DOWNLOAD_TIMEOUT);
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);
messageHandler.accept(task.request, null);
String cmd = "/usr/bin/youtube-dl -x"
+ " -f " + formatCodes + "worstaudio/bestaudio/worst/best"
+ " --audio-format=wav"
+ " --no-playlist"
+ " -o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s "
+ song.getUrl().toString();
Process exec = runCommand(cmd, 300);
InputStream in = exec.getInputStream();
String output = new String(in.readAllBytes(), Charset.defaultCharset());
String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset());
System.out.println(output);
Matcher matcher = DESTINATION_PATTERN.matcher(output);
if (matcher.find())
song.setLocation(new File(matcher.group(1)));
else if (exec.exitValue() != 0)
throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error);
// return true;
if (task.getDestination() != null)
task.getDestination().accept(song);
downloadQueue.remove(task);
messageHandler.accept(task.request, null);
} catch (Exception ex)
{
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
if (messageHandler != null)
messageHandler.accept(request, ex);
//downloadQueue.remove(task);
messageHandler.accept(task.request, ex);
downloadQueue.remove(task);
}
}
private Process runCommand(List<String> cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException
private Process runCommand(String cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException
{
System.out.println("Running command: " + cmd);
Process exec = new ProcessBuilder(cmd).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
if (timeoutSecs > 0)
// Process exec = Runtime.getRuntime().exec().split(" "));
Process exec = new ProcessBuilder(cmd.split(" ")).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
boolean done = exec.waitFor(timeoutSecs, TimeUnit.SECONDS);
if (!done)
{
scheduler.schedule(() ->
{
if (exec.isAlive())
{
exec.destroyForcibly();
System.err.println("Process " + cmd + " took too long, killing process.");
}
}, timeoutSecs, TimeUnit.SECONDS);
exec.destroyForcibly();
throw new RuntimeException("Took too long, giving up.");
}
return exec;
}
public BiConsumer<TrackRequest, Exception> getMessageHandler()
public BiConsumer<SongRequest, Exception> getMessageHandler()
{
return messageHandler;
}
public void setMessageHandler(BiConsumer<TrackRequest, Exception> messageHandler)
public void setMessageHandler(BiConsumer<SongRequest, Exception> messageHandler)
{
this.messageHandler = messageHandler;
}
public List<TrackRequest> getDownloadQueue()
public List<DownloadTask> getDownloadQueue()
{
return new ArrayList<>(inputQueue);
return downloadQueue;
}
public static class DownloadTask
{
// public static class DownloadTask
// {
//
// private final TrackRequest request;
// private final Consumer<Track> destination;
//
// public DownloadTask(TrackRequest request, Consumer<Track> destination)
// {
// this.request = request;
// this.destination = destination;
// }
//
// public TrackRequest getTrack()
// {
// return request;
// }
//
// public Consumer<Track> getDestination()
// {
// return destination;
// }
//
// }
private final SongRequest request;
private final Consumer<Song> destination;
public DownloadTask(SongRequest request, Consumer<Song> destination)
{
this.request = request;
this.destination = destination;
}
public SongRequest getSong()
{
return request;
}
public Consumer<Song> 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.YamlMapping;
import java.util.Objects;
import javax.json.JsonObject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
@ -17,14 +17,13 @@ import javax.json.JsonObject;
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 extension;
private final String resolution;
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)
{
@ -34,28 +33,6 @@ class Format
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)
{
Format format = new Format(
@ -78,44 +55,46 @@ class Format
public boolean isAudioOnly()
{
if (audioOnly)
return true;
return resolution.trim().toLowerCase().contains("audio only") || note.trim().toLowerCase().contains("tiny");
}
public void setAudioOnly(boolean audioOnly)
{
this.audioOnly = audioOnly;
return resolution.trim().toLowerCase().contains("audio only");
}
public long getSize()
{
if (size != -1)
return size;
// try to find eg. "1.32MiB" inside note
long size = Util.parseSize(note);
return size;
}
public int getSampleRate()
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 -1;
}
public int getBitrate()
{
if (samplerate != -1)
return samplerate;
// try to find eg. "51k" inside note
return Util.parseSampleRate(note);
}
public void setSampleRate(int samplerate)
{
this.samplerate = samplerate;
}
public void setSize(long size)
{
this.size = size;
Matcher matcher = BITRATE_PATTERN.matcher(note);
if (matcher.find())
{
return Integer.parseInt(matcher.group(1)) * 1000;
}
return -1;
}
public String getCode()
@ -144,42 +123,4 @@ class Format
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;
}
}

@ -1,112 +0,0 @@
/*
* 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);
}
}
}

@ -0,0 +1,366 @@
/*
* 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 java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.security.auth.login.LoginException;
import moe.nekojimi.chords.commands.*;
import moe.nekojimi.musicsearcher.Result;
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.MessageBuilder;
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 class Main extends ListenerAdapter
{
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;
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws LoginException
{
// 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()
{
log("INFO", "Starting up...");
downloader = new Downloader();
downloader.setMessageHandler(downloaderMessageHandler);
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);
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 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,50 +7,54 @@ package moe.nekojimi.chords;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.*;
import net.dv8tion.jda.api.audio.AudioSendHandler;
import org.apache.commons.io.input.buffer.CircularByteBuffer;
/**
*
* @author jimj316
*/
public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track>
public class MusicHandler implements AudioSendHandler, Closeable
{
// private QueueThing<?, Track> queueManager;
private QueueManager queueManager;
// private final LinkedList<Track> trackQueue = new LinkedList<>();
// private final LinkedList<Song> songQueue = new LinkedList<>();
// 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 int byteCount;
private boolean arrayErr = false;
private Consumer<Track> nowPlayingConsumer;
private Consumer<Song> nowPlayingConsumer;
public void setNowPlayingConsumer(Consumer<Track> nowPlayingConsumer)
public void setNowPlayingConsumer(Consumer<Song> nowPlayingConsumer)
{
this.nowPlayingConsumer = nowPlayingConsumer;
}
private Track currentTrack;
// private TrackPlayer player;
private final List<TrackPlayer> playingTracks = new ArrayList<>();
private Song currentSong;
private TrackPlayer player;
public MusicHandler()
{
}
private File debugOutFile;
private BufferedOutputStream debugOut;
@Override
public void accept(Track t)
public MusicHandler()
{
play(t);
try
{
debugOutFile = new File("debug.wav");
if (debugOutFile.exists())
debugOutFile.delete();
debugOutFile.createNewFile();
debugOut = new BufferedOutputStream(new FileOutputStream(debugOutFile));
} catch (IOException ex)
{
Logger.getLogger(MusicHandler.class.getName()).log(Level.SEVERE, null, ex);
}
}
void setQueueManager(QueueManager manager)
@ -58,74 +62,72 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
queueManager = manager;
}
public void playOver(Track track)
public void playNext()
{
nextSong(true);
}
public boolean play(Track track)
// public boolean restartSong()
// {
//// songQueue.addFirst(currentSong);
// currentSong = null;
// return nextSong(true);
// }
private boolean nextSong()
{
return play(track, false);
return nextSong(false);
}
public boolean play(Track track, boolean immediate)
public boolean nextSong(boolean immediate)
{
if (track == currentTrack)
return false;
if (immediate)
{
System.out.println("Immediate next - clearing buffer");
playingTracks.clear();
audioBuffer.clear();
}
try
{
if (currentTrack != null)
if (currentSong != null)
{
if (!currentSong.isKept())
currentSong.delete();
currentSong = null;
}
if (player != null)
{
if (!currentTrack.isKept())
currentTrack.delete();
currentTrack = null;
player.close();
player = null;
}
currentTrack = track;
currentSong = queueManager.nextSongNeeded();
if (nowPlayingConsumer != null)
nowPlayingConsumer.accept(currentTrack);
if (currentTrack == null)
nowPlayingConsumer.accept(currentSong);
if (currentSong == null)
{
System.out.println("End of queue.");
debugOut.flush();
return false;
}
// System.out.println("Playing track " + currentTrack.getLocation().getAbsolutePath());
System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath());
arrayErr = false;
byteCount = 3840;
TrackPlayer player = new TrackPlayer(currentTrack);
playingTracks.add(player);
player = new TrackPlayer(currentSong);
player.start();
// System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes());
return true;
} catch (UnsupportedAudioFileException | IOException ex)
{
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex);
currentTrack = null;
requestTrack();
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
} finally
{
}
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()
{
return !playingTracks.isEmpty();
return player != null;
}
public boolean isShouldPlay()
@ -136,19 +138,14 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
public void setShouldPlay(boolean shouldPlay)
{
if (!this.shouldPlay && shouldPlay)
requestTrack();
nextSong();
this.shouldPlay = shouldPlay;
}
@Override
public boolean canProvide()
{
if (playingTracks.isEmpty())
return false;
for (TrackPlayer player : playingTracks)
if (player.has(1))
return true;
return false;
return player != null && player.has(1);
// If we have something in our buffer we can provide it to the send system
// return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay;
}
@ -157,35 +154,27 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
public ByteBuffer provide20MsAudio()
{
ByteBuffer ret = ByteBuffer.allocate(byteCount);
while (ret.position() < byteCount && !playingTracks.isEmpty())
while (ret.position() < byteCount && player != null)
{
boolean outOfInput = true;
List<ByteBuffer> mixes = new ArrayList<>();
List<TrackPlayer> emptyPlayers = new ArrayList<>();
for (TrackPlayer player : playingTracks)
// System.out.println("Position: " + ret.position() + " Remaining: " + ret.remaining());
try
{
try
{
ByteBuffer read = player.read(ret.remaining());
if (ret.limit() + read.position() >= byteCount)
outOfInput = false;
mixes.add(read);
// 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)
ByteBuffer read = player.read(ret.remaining());
// System.out.println("SAMPLES from player: " + Util.printSamples(read));
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
// System.out.println("Read: " + read.remaining());
ret.put(read);
} catch (TrackPlayer.OutOfInputException | IOException ex)
{
boolean foundNext = requestTrack();
// System.out.println("Track ended, starting next.");
boolean foundNext = nextSong();
if (!foundNext)
{
// System.out.println("Out of tracks!");
break;
}
}
}
@ -195,10 +184,9 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
return ret;
}
public Track getCurrentTrack()
public Song getCurrentSong()
{
return currentTrack;
return currentSong;
}
@Override
@ -212,48 +200,4 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
{
}
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;
}
}

@ -16,28 +16,19 @@
*/
package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlMapping;
import com.amihaiemil.eoyaml.YamlSequence;
import com.amihaiemil.eoyaml.YamlSequenceBuilder;
import java.net.MalformedURLException;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author jimj316
*/
public class Playlist implements Consumer<Track>
public class Playlist
{
private static final int SHUFFLE_DONT_REPEAT_LAST = 3;
private final String name;
private final List<Track> tracks = new ArrayList<>();
private final LinkedList<Track> playHistory = new LinkedList<>();
private final List<Song> songs = new ArrayList<>();
public Playlist(String name)
{
@ -46,37 +37,18 @@ public class Playlist implements Consumer<Track>
public YamlMapping toYaml()
{
YamlSequenceBuilder trackList = Yaml.createYamlSequenceBuilder();
for (Track track : tracks)
trackList = trackList.add(track.toYaml());
return Yaml.createYamlMappingBuilder()
.add("name", name)
.add("tracks", trackList.build())
.build();
throw new UnsupportedOperationException("Not supported yet.");
}
public static Playlist fromYaml(YamlMapping yaml)
{
Playlist ret = new Playlist(yaml.string("name"));
YamlSequence trackList = yaml.value("tracks").asSequence();
for (int i = 0; i < trackList.size(); i++)
{
try
{
ret.addTrack(Track.fromYaml(trackList.yamlMapping(i)));
} catch (MalformedURLException ex)
{
Logger.getLogger(Playlist.class.getName()).log(Level.SEVERE, null, ex);
}
}
return ret;
throw new UnsupportedOperationException("Not supported yet.");
}
public void addTrack(Track track)
public void addSong(Song song)
{
track.setKept(true);
tracks.add(track);
song.setKept(true);
songs.add(song);
}
public String getName()
@ -84,39 +56,14 @@ public class Playlist implements Consumer<Track>
return name;
}
public List<Track> getTracks()
{
return tracks;
}
public Track getNextTrack()
public List<Song> getSongs()
{
Track ret;
// copy the track list
List<Track> toShuffle = new LinkedList<>(tracks);
// remove play history from candidates, latest first, unless we'd have less than 2 options
for (int i = playHistory.size() - 1; i >= 0; i--)
{
if (toShuffle.size() <= 2)
break;
toShuffle.remove(playHistory.get(i));
}
Collections.shuffle(toShuffle);
ret = toShuffle.get(0);
playHistory.add(ret);
if (playHistory.size() > SHUFFLE_DONT_REPEAT_LAST)
playHistory.remove();
return ret;
return songs;
}
@Override
public void accept(Track t)
Song getNextSong()
{
addTrack(t);
throw new UnsupportedOperationException("Not supported yet.");
}
}

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

@ -1,211 +0,0 @@
/*
* 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);
// }
}

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

@ -14,52 +14,70 @@
* 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.commands;
package moe.nekojimi.chords;
import java.net.URL;
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.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import net.dv8tion.jda.internal.entities.DataMessage;
/**
*
* @author jimj316
*/
public class Invocation
public class SongRequest
{
private Message requestMessage;
private Message responseMessage;
private final MessageReceivedEvent event;
private final List<String> args;
private String query;
private URL url;
public Invocation(MessageReceivedEvent event, List<String> args)
{
this.event = event;
this.args = args;
}
private List<Result> searchResults;
public MessageReceivedEvent getEvent()
{
return event;
}
private Result result;
public List<String> getArgs()
{
return args;
}
private Song song;
@SuppressWarnings("null")
public void respond(String text)
{
MessageAction action = null;
if (responseMessage == null)
{
responseMessage = requestMessage.reply(text).complete();
action = requestMessage.reply(text);
} else
{
responseMessage = responseMessage.editMessage(text).complete();
action = responseMessage.editMessage(text);
}
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()
@ -82,4 +100,34 @@ public class Invocation
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;
}
}

@ -5,7 +5,6 @@
*/
package moe.nekojimi.chords;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -22,60 +21,94 @@ import org.apache.commons.io.input.buffer.CircularByteBuffer;
*
* @author jimj316
*/
public class TrackPlayer implements Closeable
public class TrackPlayer extends Thread implements Closeable
{
private static final boolean DEBUG_PRINT = false;
private static final int DESIRED_BUFFER_SIZE = 3840 * 500;
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 AudioInputStream input;
private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam
private boolean ended = false;
public TrackPlayer(Track track) throws UnsupportedAudioFileException, IOException
private final Object fillBufferWait = new Object();
private final Object bufferFilledWait = new Object();
public TrackPlayer(Song song) throws UnsupportedAudioFileException, IOException
{
setName("TrackPlayer disk thread: " + song.toString());
AudioInputStream in = AudioSystem.getAudioInputStream(song.getLocation());
AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT;
input = AudioSystem.getAudioInputStream(decodedFormat, in);
// fillBuffer(false);
}
public TrackPlayer(AudioInputStream input) throws IOException
{
this.input = input;
// fillBuffer(false);
}
public void end()
{
ended = true;
}
@Override
public void run()
{
AudioInputStream in = null;
AudioFormat decodedFormat = null;
int retry = 0;
while (in == null)
while (!ended)
{
try
int bytes;
synchronized (audioBuffer)
{
in = AudioSystem.getAudioInputStream(new BufferedInputStream(track.getInputStream()));
decodedFormat = AudioSendHandler.INPUT_FORMAT;
break; // it worked!
} catch (Exception ex)
bytes = audioBuffer.getCurrentNumberOfBytes();
}
if (bytes >= DESIRED_BUFFER_SIZE)
{
retry++;
if (retry < RETRY_COUNT)
synchronized (fillBufferWait)
{
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)
if (DEBUG_PRINT)
System.out.println("DISK THREAD: waiting");
fillBufferWait.wait(5000);
} catch (InterruptedException ex)
{
// this is normal
}
} else
if (DEBUG_PRINT)
System.out.println("DISK THREAD: kicked");
}
}
try
{
boolean notAtEnd = fillBuffer(true);
if (!notAtEnd)
{
throw ex;
if (DEBUG_PRINT)
System.out.println("DISK THREAD: end of input");
ended = true;
}
synchronized (bufferFilledWait)
{
if (DEBUG_PRINT)
System.out.println("DISK THREAD: buffer filled; kicking read thread");
bufferFilledWait.notifyAll();
}
} catch (IOException ex)
{
Logger.getLogger(TrackPlayer.class.getName()).log(Level.SEVERE, null, ex);
ended = true;
}
}
input = AudioSystem.getAudioInputStream(decodedFormat, in);
fillBuffer(false);
}
public TrackPlayer(AudioInputStream input) throws IOException
{
this.input = input;
fillBuffer(false);
if (DEBUG_PRINT)
System.out.println("DISK THREAD ENDED");
bufferFilledWait.notifyAll(); // kick read thread in case it was waiting for us
}
public boolean has(int byteCount)
@ -86,14 +119,17 @@ public class TrackPlayer implements Closeable
public ByteBuffer read(int length) throws IOException
{
boolean filled = fillBuffer(true);
checkBuffer();
// if (!filled)
// throw new OutOfInputException();
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)
{
ended = true;
throw new OutOfInputException();
}
byte[] data = new byte[toRead];
audioBuffer.read(data, 0, data.length);
@ -101,6 +137,44 @@ public class TrackPlayer implements Closeable
return ByteBuffer.wrap(data); // Wrap this in a java.nio.ByteBuffer
}
private boolean checkBuffer()
{
synchronized (fillBufferWait)
{
if (!ended)
{
if (DEBUG_PRINT)
System.out.println("READ THREAD: kicking disk thread");
fillBufferWait.notifyAll(); // kick the disk thread to fill the buffer if needed
}
}
int bytes;
synchronized (audioBuffer)
{
bytes = audioBuffer.getCurrentNumberOfBytes();
}
if (bytes == 0 && !ended)
{
synchronized (bufferFilledWait)
{
try
{
System.out.println("READ THREAD: waiting for disk thread");
bufferFilledWait.wait(5000); // wait for disk thread to fill the buffer
} catch (InterruptedException ex)
{
}
if (DEBUG_PRINT)
System.out.println("READ THREAD: kicked");
}
}
synchronized (audioBuffer)
{
bytes = audioBuffer.getCurrentNumberOfBytes();
}
return bytes > 0;
}
/**
*
* @param canSkip
@ -113,19 +187,20 @@ public class TrackPlayer implements Closeable
while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
{
boolean read = readData();
if (!read && !canSkip)
return false;
else if (fails < MAX_READ_FAILS)
fails++;
else
return false;
if (!read)
{
if (canSkip && fails < MAX_READ_FAILS)
fails++;
else
return false;
}
}
return true;
}
/**
*
* @return true if any data was read; false otherwise
* @return true if there is still data to be read
*/
private boolean readData() throws IOException
{
@ -135,23 +210,32 @@ public class TrackPlayer implements Closeable
{
// if (din.available() == 0)
// return false;
int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
int space = audioBuffer.getSpace();
int bytesToRead;
int space;
synchronized (audioBuffer)
{
bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
space = audioBuffer.getSpace();
}
if (input.available() > 0 && input.available() < bytesToRead)
bytesToRead = input.available();
if (bytesToRead > space)
bytesToRead = space;
if (bytesToRead == 0)
return false;
return true;
byte[] bytes = new byte[bytesToRead];
// byte[] bytes = din.readNBytes(bytesToRead);
int read = input.read(bytes);
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
if (DEBUG_PRINT)
System.out.println(" Space:" + space + " Available: " + input.available() + " To read: " + bytesToRead + " Read: " + read);
if (read < 0)
return false;
// queue.add(bytes);
audioBuffer.add(bytes, 0, read);
synchronized (audioBuffer)
{
audioBuffer.add(bytes, 0, read);
}
// System.out.println("SAMPLES player buff: " + Util.printSamples(audioBuffer));
} catch (ArrayIndexOutOfBoundsException ex)
{
@ -165,11 +249,10 @@ public class TrackPlayer implements Closeable
public void close() throws IOException
{
input.close(); //q
input.close();
}
public static class OutOfInputException extends RuntimeException
{

@ -1,210 +0,0 @@
/*
* 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,8 +1,6 @@
package moe.nekojimi.chords;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*
* Copyright (C) 2022 jimj316
@ -27,10 +25,6 @@ import java.util.regex.Pattern;
*/
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)
{
StringBuilder sb = new StringBuilder();
@ -40,56 +34,4 @@ public class Util
}
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,7 +5,9 @@
*/
package moe.nekojimi.chords.commands;
import moe.nekojimi.chords.Chords;
import java.util.List;
import moe.nekojimi.chords.Main;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
/**
*
@ -14,35 +16,26 @@ import moe.nekojimi.chords.Chords;
public abstract class Command
{
protected final Chords bot;
protected final Main bot;
protected final String keyword;
protected String documentation;
public Command(Chords bot, String keyword)
public Command(Main bot, String keyword)
{
this.bot = bot;
this.keyword = keyword;
}
public abstract void call(Invocation invocation);
public abstract void call(GuildMessageReceivedEvent event, List<String> arg);
public String getKeyword()
{
return keyword;
}
public String argumentDescription()
public String getDocumentation()
{
return ""; // most commands take no arguments
}
public String synopsis()
{
throw new UnsupportedOperationException("Not supported yet.");
}
//
public String help()
{
throw new UnsupportedOperationException("Not supported yet.");
return documentation;
}
}

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

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

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

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

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

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

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

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

Loading…
Cancel
Save