Compare commits

..

No commits in common. 'master' and 'new-trackplayer' have entirely different histories.

  1. 6
      .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. 493
      src/main/java/moe/nekojimi/chords/Downloader.java
  12. 123
      src/main/java/moe/nekojimi/chords/Format.java
  13. 112
      src/main/java/moe/nekojimi/chords/LocalBindSocketFactory.java
  14. 290
      src/main/java/moe/nekojimi/chords/Main.java
  15. 279
      src/main/java/moe/nekojimi/chords/MusicHandler.java
  16. 80
      src/main/java/moe/nekojimi/chords/Playlist.java
  17. 187
      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. 108
      src/main/java/moe/nekojimi/chords/Song.java
  21. 43
      src/main/java/moe/nekojimi/chords/TrackPlayer.java
  22. 210
      src/main/java/moe/nekojimi/chords/TrackRequest.java
  23. 58
      src/main/java/moe/nekojimi/chords/Util.java
  24. 25
      src/main/java/moe/nekojimi/chords/commands/Command.java
  25. 29
      src/main/java/moe/nekojimi/chords/commands/HelpCommand.java
  26. 85
      src/main/java/moe/nekojimi/chords/commands/Invocation.java
  27. 73
      src/main/java/moe/nekojimi/chords/commands/JoinCommand.java
  28. 8
      src/main/java/moe/nekojimi/chords/commands/LeaveCommand.java
  29. 46
      src/main/java/moe/nekojimi/chords/commands/PlayCommand.java
  30. 50
      src/main/java/moe/nekojimi/chords/commands/QueueCommand.java
  31. 22
      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

6
.gitignore vendored

@ -19,7 +19,7 @@
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
target/* target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
@ -55,9 +55,5 @@ dist/
nbdist/ nbdist/
.nb-gradle/ .nb-gradle/
# Git probably
*.orig
# Secrets! # Secrets!
secrets.yml 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. Any value defined here will override the pom.xml file value but is only applicable to the current project.
--> -->
<netbeans.hint.license>gpl30</netbeans.hint.license> <netbeans.hint.license>gpl30</netbeans.hint.license>
<org-netbeans-modules-javascript2-requirejs.enabled>true</org-netbeans-modules-javascript2-requirejs.enabled>
</properties> </properties>
</project-shared-configuration> </project-shared-configuration>

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

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

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

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

@ -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,290 @@
/*
* 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.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
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.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 JDA jda;
private final Map<String, Command> commands = new HashMap<>();
private final Command helpCommand;
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
);
JDABuilder builder = JDABuilder.createDefault(args[0], intents);
// Disable parts of the cache
builder.disableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE);
// 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);
JDA jda = builder.build();
listener.setJda(jda);
}
public Main()
{
log("INFO", "Starting up...");
downloader = new Downloader();
downloader.setMessageHandler((Song song, Exception ex) ->
{
TextChannel channel = jda.getTextChannelById(song.getRequestedIn());
// String bracketNo = "[" + song.getNumber() + "] ";
if (channel != null)
if (ex == null)
if (song.getLocation() != null)
{
channel.sendMessage(/*bracketNo + */"Finished downloading " + song + " for " + song.getRequestedBy() + ", added to queue!").queue();
log("DOWN", "Downloaded " + song);
} else
{
Format format = song.getBestFormat();
String formatDetails = "";
if (format != null)
{
formatDetails = " (" + format.getBitrate() / 1000 + "k, " + String.format("%.2f", format.getSize() / (1024.0 * 1024.0)) + "MiB)";
}
channel.sendMessage(/*bracketNo + */"Now downloading " + song + formatDetails + " for " + song.getRequestedBy() + " ...").queue();
log("DOWN", "Downloading " + song + "...");
}
else
{
channel.sendMessage(/*bracketNo + */"Failed to download " + song + " for " + song.getRequestedBy() + "! Reason: " + ex.getMessage()).queue();
log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage());
}
});
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
addCommand(new JoinCommand(this));
addCommand(new LeaveCommand(this));
addCommand(new PlayCommand(this));
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
{
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(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;
}
/**
* 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();
// 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;
}
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 int getTrackNumber()
{
return trackNumber;
}
}

@ -6,199 +6,241 @@
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.io.*; import java.io.*;
import moe.nekojimi.chords.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.LinkedList;
import java.util.List; import java.util.Queue;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import net.dv8tion.jda.api.audio.AudioSendHandler; import net.dv8tion.jda.api.audio.AudioSendHandler;
import org.apache.commons.io.input.buffer.CircularByteBuffer;
/** /**
* *
* @author jimj316 * @author jimj316
*/ */
public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track> public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Song>
{ {
// private QueueThing<?, Track> queueManager; private final LinkedList<Song> songQueue = new LinkedList<>();
private QueueManager queueManager;
// private final LinkedList<Track> trackQueue = new LinkedList<>();
// private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>(); // private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
// private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
private boolean shouldPlay = true; private boolean playing = true;
private int byteCount; private int byteCount;
private boolean arrayErr = false; private boolean arrayErr = false;
private Consumer<Track> nowPlayingConsumer;
public void setNowPlayingConsumer(Consumer<Track> nowPlayingConsumer) private Song currentSong;
{ private TrackPlayer player;
this.nowPlayingConsumer = nowPlayingConsumer;
}
private Track currentTrack; private File debugOutFile;
// private TrackPlayer player; private BufferedOutputStream debugOut;
private final List<TrackPlayer> playingTracks = new ArrayList<>();
public MusicHandler() public MusicHandler()
{ {
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);
}
} }
@Override public void addSong(Song song)
public void accept(Track t)
{ {
play(t); System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath());
songQueue.add(song);
if (!canProvide() && playing)
nextSong();
} }
void setQueueManager(QueueManager manager) public boolean removeSong(int i)
{
try
{ {
queueManager = manager; songQueue.remove(i);
return true;
} catch (ArrayIndexOutOfBoundsException ex)
{
return false;
}
} }
public boolean removeSong(Song song)
public void playOver(Track track)
{ {
return songQueue.remove(song);
} }
public boolean play(Track track) public boolean restartSong()
{ {
return play(track, false); songQueue.addFirst(currentSong);
currentSong = null;
return nextSong(true);
} }
public boolean play(Track track, boolean immediate) private boolean nextSong()
{ {
if (track == currentTrack) return nextSong(false);
return false; }
public boolean nextSong(boolean immediate)
{
if (immediate) if (immediate)
{ {
System.out.println("Immediate next - clearing buffer"); System.out.println("Immediate next - clearing buffer");
playingTracks.clear(); audioBuffer.clear();
} }
try try
{ {
if (currentTrack != null) if (currentSong != null)
{ {
if (!currentTrack.isKept()) if (!currentSong.isKept())
currentTrack.delete(); currentSong.delete();
currentTrack = null; currentSong = null;
} }
currentTrack = track; currentSong = songQueue.poll();
if (nowPlayingConsumer != null) if (currentSong == null)
nowPlayingConsumer.accept(currentTrack);
if (currentTrack == null)
{ {
System.out.println("End of queue.");
debugOut.flush();
return false; return false;
} }
// System.out.println("Playing track " + currentTrack.getLocation().getAbsolutePath()); System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath());
arrayErr = false; arrayErr = false;
byteCount = 3840; byteCount = 3840;
TrackPlayer player = new TrackPlayer(currentTrack); player = new TrackPlayer(currentSong);
playingTracks.add(player);
// System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes()); // System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes());
return true; return true;
} catch (UnsupportedAudioFileException | IOException ex) } catch (UnsupportedAudioFileException | IOException ex)
{ {
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
currentTrack = null;
requestTrack();
} finally } finally
{ {
} }
return false; return false;
} }
public boolean requestTrack()
{
if (isPlaying())
System.out.println("How did we get here?");
System.out.println("MusicHandler requesting track...");
List<QueueThing.Promise<Track, Track>> request = queueManager.request(1, this);
// Queuemanager will syncronously attempt to call play()
return !playingTracks.isEmpty();
}
public boolean isPlaying() public boolean isPlaying()
{ {
return !playingTracks.isEmpty(); return playing;
}
public boolean isShouldPlay()
{
return shouldPlay;
} }
public void setShouldPlay(boolean shouldPlay) public void setPlaying(boolean playing)
{ {
if (!this.shouldPlay && shouldPlay) if (!this.playing && playing)
requestTrack(); nextSong();
this.shouldPlay = shouldPlay; this.playing = playing;
} }
@Override @Override
public boolean canProvide() public boolean canProvide()
{ {
if (playingTracks.isEmpty()) return player != null && player.has(1);
return false;
for (TrackPlayer player : playingTracks)
if (player.has(1))
return true;
return false;
// If we have something in our buffer we can provide it to the send system // If we have something in our buffer we can provide it to the send system
// return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay; // return audioBuffer.getCurrentNumberOfBytes() > byteCount && playing;
} }
@Override @Override
public ByteBuffer provide20MsAudio() public ByteBuffer provide20MsAudio()
{ {
ByteBuffer ret = ByteBuffer.allocate(byteCount); 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()); ByteBuffer read = player.read(ret.remaining());
if (ret.limit() + read.position() >= byteCount) // System.out.println("SAMPLES from player: " + Util.printSamples(read));
outOfInput = false;
mixes.add(read); System.out.println("Read: " + read.remaining());
// ret.put(read); ret.put(read);
} catch (TrackPlayer.OutOfInputException | IOException ex) } catch (TrackPlayer.OutOfInputException | IOException ex)
{ {
// System.out.println("Track player " + player + " stopped giving input: " + ex.getMessage()); System.out.println("Track ended, starting next.");
emptyPlayers.add(player); boolean foundNext = nextSong();
// System.out.println("Track ended, starting next.");
// outOfInput = true;
}
}
playingTracks.removeAll(emptyPlayers);
ret.put(mixBuffers(mixes));
if (outOfInput)
{
boolean foundNext = requestTrack();
if (!foundNext) if (!foundNext)
{
System.out.println("Out of tracks!");
break; break;
} }
}
} }
// System.out.println("Buffer filled, submitting."); System.out.println("Buffer filled, submitting.");
ret.rewind(); // required apparently, if returned buf has pos > 0 you get silence ret.rewind(); // required apparently, if returned buf has pos > 0 you get silence
assert ret.hasArray(); // output MUST be array backed assert ret.hasArray(); // output MUST be array backed
return ret; return ret;
} }
public Track getCurrentTrack() // private void fillBuffer(boolean canSkip)
// {
// // use what we have in our buffer to send audio as PCM
// while (audioBuffer.getCurrentNumberOfBytes() < DESIRED_BUFFER_SIZE)
// if (!readData())
// if (!canSkip || !nextSong())
// break;
// }
//
// private boolean readData()
// {
// if (din == null)
// return false;
// try
// {
// // if (din.available() == 0)
// // return false;
// int bytesToRead = DESIRED_BUFFER_SIZE - audioBuffer.getCurrentNumberOfBytes();
// int space = audioBuffer.getSpace();
// if (din.available() > 0 && din.available() < bytesToRead)
// bytesToRead = din.available();
// if (bytesToRead > space)
// bytesToRead = space;
// if (bytesToRead == 0)
// return false;
// byte[] bytes = new byte[bytesToRead];
// // byte[] bytes = din.readNBytes(bytesToRead);
// int read = din.read(bytes);
//// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
// if (read < 0)
// return false;
//// queue.add(bytes);
//
// audioBuffer.add(bytes, 0, read);
// } catch (IOException ex)
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// } catch (ArrayIndexOutOfBoundsException ex)
// {
// if (!arrayErr)
// arrayErr = true;
// else
// {
// Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
// return false;
// }
// }
// return true;
// }
public Queue<Song> getSongQueue()
{
return songQueue;
}
public Song getCurrentSong()
{ {
return currentTrack; return currentSong;
} }
@Override @Override
@ -212,48 +254,9 @@ public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track
{ {
} }
private ByteBuffer mixBuffers(List<ByteBuffer> mixes) @Override
{ public void accept(Song t)
// 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()) addSong(t);
return false;
playingTracks.clear();
currentTrack = null;
requestTrack();
return true;
} }
} }

@ -16,28 +16,19 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import com.amihaiemil.eoyaml.Yaml;
import com.amihaiemil.eoyaml.YamlMapping; import com.amihaiemil.eoyaml.YamlMapping;
import com.amihaiemil.eoyaml.YamlSequence; import java.util.ArrayList;
import com.amihaiemil.eoyaml.YamlSequenceBuilder; import java.util.List;
import java.net.MalformedURLException;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* *
* @author jimj316 * @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 String name;
private final List<Track> tracks = new ArrayList<>(); private final List<Song> songs = new ArrayList<>();
private final LinkedList<Track> playHistory = new LinkedList<>();
public Playlist(String name) public Playlist(String name)
{ {
@ -46,37 +37,18 @@ public class Playlist implements Consumer<Track>
public YamlMapping toYaml() public YamlMapping toYaml()
{ {
YamlSequenceBuilder trackList = Yaml.createYamlSequenceBuilder(); throw new UnsupportedOperationException("Not supported yet.");
for (Track track : tracks)
trackList = trackList.add(track.toYaml());
return Yaml.createYamlMappingBuilder()
.add("name", name)
.add("tracks", trackList.build())
.build();
} }
public static Playlist fromYaml(YamlMapping yaml) public static Playlist fromYaml(YamlMapping yaml)
{ {
Playlist ret = new Playlist(yaml.string("name")); throw new UnsupportedOperationException("Not supported yet.");
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;
} }
public void addTrack(Track track) public void addSong(Song song)
{ {
track.setKept(true); song.setKept(true);
tracks.add(track); songs.add(song);
} }
public String getName() public String getName()
@ -84,39 +56,9 @@ public class Playlist implements Consumer<Track>
return name; return name;
} }
public List<Track> getTracks() public List<Song> getSongs()
{
return tracks;
}
public Track getNextTrack()
{
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;
}
@Override
public void accept(Track t)
{ {
addTrack(t); return songs;
} }
} }

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

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

@ -5,7 +5,6 @@
*/ */
package moe.nekojimi.chords; package moe.nekojimi.chords;
import java.io.BufferedInputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -27,8 +26,6 @@ public class TrackPlayer implements Closeable
private static final int DESIRED_BUFFER_SIZE = 3840 * 500; private static final int DESIRED_BUFFER_SIZE = 3840 * 500;
private static final int MAX_READ_FAILS = 3; private static final int MAX_READ_FAILS = 3;
private static final int RETRY_COUNT = 8;
private static final int RETRY_DELAY = 100;
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024); private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
@ -36,38 +33,10 @@ public class TrackPlayer implements Closeable
private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam
public TrackPlayer(Track track) throws UnsupportedAudioFileException, IOException public TrackPlayer(Song song) throws UnsupportedAudioFileException, IOException
{ {
AudioInputStream in = null; AudioInputStream in = AudioSystem.getAudioInputStream(song.getLocation());
AudioFormat decodedFormat = null; AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT;
int retry = 0;
while (in == null)
{
try
{
in = AudioSystem.getAudioInputStream(new BufferedInputStream(track.getInputStream()));
decodedFormat = AudioSendHandler.INPUT_FORMAT;
break; // it worked!
} catch (Exception ex)
{
retry++;
if (retry < RETRY_COUNT)
{
System.err.println("Open file " + track.getLocation() + " failed because " + ex.getMessage() + " retry " + retry + "...");
try
{
Thread.sleep(((long) Math.pow(2, retry)) * RETRY_DELAY);
} catch (InterruptedException ex1)
{
}
} else
{
throw ex;
}
}
}
input = AudioSystem.getAudioInputStream(decodedFormat, in); input = AudioSystem.getAudioInputStream(decodedFormat, in);
fillBuffer(false); fillBuffer(false);
} }
@ -78,7 +47,7 @@ public class TrackPlayer implements Closeable
fillBuffer(false); fillBuffer(false);
} }
public boolean has(int byteCount) boolean has(int byteCount)
{ {
// return true; // return true;
return audioBuffer.getCurrentNumberOfBytes() >= byteCount; return audioBuffer.getCurrentNumberOfBytes() >= byteCount;
@ -163,13 +132,13 @@ public class TrackPlayer implements Closeable
return true; return true;
} }
@Override
public void close() throws IOException public void close() throws IOException
{ {
input.close(); //q input.close();
} }
public static class OutOfInputException extends RuntimeException 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; package moe.nekojimi.chords;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/* /*
* Copyright (C) 2022 jimj316 * Copyright (C) 2022 jimj316
@ -27,10 +25,6 @@ import java.util.regex.Pattern;
*/ */
public class Util public class Util
{ {
private static final Pattern SIZE_PATTERN = Pattern.compile("\\b([0-9]+\\.?[0-9]*)([kkMmGg])i?[bB]\\b");
private static final Pattern SAMPLE_RATE_PATTERN = Pattern.compile("\\b([0-9]+)k(b(ps?))?\\b");
public static String printSamples(ByteBuffer buf) public static String printSamples(ByteBuffer buf)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@ -40,56 +34,4 @@ public class Util
} }
return sb.toString(); return sb.toString();
} }
public static int parseSampleRate(String input)
{
Matcher matcher = SAMPLE_RATE_PATTERN.matcher(input);
if (matcher.find())
{
return Integer.parseInt(matcher.group(1)) * 1000;
}
return -1;
}
public static long parseSize(String note)
{
Matcher matcher = SIZE_PATTERN.matcher(note);
if (matcher.find())
{
double value = Double.parseDouble(matcher.group(1));
String mag = matcher.group(2).toUpperCase();
long mult = 1;
switch (mag)
{
case "K":
mult = 1024;
break;
case "M":
mult = 1024 * 1024;
break;
case "G":
mult = 1024 * 1024 * 1024;
break;
}
value *= mult;
return (long) value;
}
return -1L;
}
public static String formatProgressBar(double progressPercent, String[] symbols, int barLength)
{
String ret = "";
double filledSegments = (progressPercent / 100.0) * barLength;
for (int i = 0; i < barLength; i++)
{
double thisSegment = Math.min(1, Math.max(0, filledSegments - i));
int symbolIdx = (int) Math.floor(thisSegment * symbols.length);
String symbol = symbols[symbolIdx];
ret += symbol;
}
return ret;
}
} }

@ -5,7 +5,9 @@
*/ */
package moe.nekojimi.chords.commands; 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 public abstract class Command
{ {
protected final Chords bot; protected final Main bot;
protected final String keyword; protected final String keyword;
protected String documentation;
public Command(Chords bot, String keyword) public Command(Main bot, String keyword)
{ {
this.bot = bot; this.bot = bot;
this.keyword = keyword; this.keyword = keyword;
} }
public abstract void call(Invocation invocation); public abstract void call(GuildMessageReceivedEvent event, List<String> arg);
public String getKeyword() public String getKeyword()
{ {
return keyword; return keyword;
} }
public String argumentDescription() public String getDocumentation()
{ {
return ""; // most commands take no arguments return documentation;
}
public String synopsis()
{
throw new UnsupportedOperationException("Not supported yet.");
}
//
public String help()
{
throw new UnsupportedOperationException("Not supported yet.");
} }
} }

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

@ -1,85 +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.commands;
import java.util.List;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
/**
*
* @author jimj316
*/
public class Invocation
{
private Message requestMessage;
private Message responseMessage;
private final MessageReceivedEvent event;
private final List<String> args;
public Invocation(MessageReceivedEvent event, List<String> args)
{
this.event = event;
this.args = args;
}
public MessageReceivedEvent getEvent()
{
return event;
}
public List<String> getArgs()
{
return args;
}
@SuppressWarnings("null")
public void respond(String text)
{
if (responseMessage == null)
{
responseMessage = requestMessage.reply(text).complete();
} else
{
responseMessage = responseMessage.editMessage(text).complete();
}
}
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;
}
}

@ -6,51 +6,36 @@
package moe.nekojimi.chords.commands; package moe.nekojimi.chords.commands;
import java.util.List; 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.*;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public class JoinCommand extends Command public class JoinCommand extends Command
{ {
public JoinCommand(Chords bot) public JoinCommand(Main bot)
{ {
super(bot, "join"); super(bot, "join");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> args)
{ {
MessageReceivedEvent event = invocation.getEvent(); TextChannel textChannel = event.getChannel();
List<String> args = invocation.getArgs();
MessageChannel textChannel = event.getChannel();
VoiceChannel channel = null; VoiceChannel channel = null;
if (args.isEmpty() || args.get(0).isBlank()) if (args.isEmpty() || args.get(0).isBlank())
{ {
final Member member = event.getMessage().getMember(); GuildVoiceState voiceState = event.getMember().getVoiceState();
if (member != null) if (voiceState != null && voiceState.inVoiceChannel())
{ channel = voiceState.getChannel();
GuildVoiceState voiceState = member.getVoiceState(); else
if (voiceState != null && voiceState.inAudioChannel())
channel = voiceState.getChannel().asVoiceChannel();
}
if (channel == null)
{ {
Guild guild = event.getMessage().getGuild(); Guild guild = event.getMessage().getGuild();
List<VoiceChannel> voiceChannels = guild.getVoiceChannels(); List<VoiceChannel> voiceChannels = guild.getVoiceChannels();
System.out.println("Finding channel to join..."); Optional<VoiceChannel> findFirst = voiceChannels.stream().filter((t) -> !t.getMembers().isEmpty()).findFirst();
for (VoiceChannel voiceChannel : voiceChannels) channel = findFirst.orElseThrow(() -> new RuntimeException("Cannot find any voice channels with people in!"));
{
List<Member> members = voiceChannel.getMembers();
if (!members.isEmpty())
channel = voiceChannel;
System.out.println("\t" + voiceChannel.getName() + " " + members.size());
} }
}
if (channel == null)
throw new RuntimeException("Cannot find any voice channels with people in!");
} else } else
{ {
String arg0 = args.get(0); String arg0 = args.get(0);
@ -67,20 +52,40 @@ public class JoinCommand extends Command
if (channel == null) // I have no idea what you want mr user if (channel == null) // I have no idea what you want mr user
{ {
// onUnknownChannel(textChannel, arg0); // Let the user know about our failure onUnknownChannel(textChannel, arg0); // Let the user know about our failure
throw new RuntimeException("Unable to connect to \"" + channel.getName() + "\", no such channel!"); return;
} }
} }
bot.connectTo(channel); // We found a channel to connect to! 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; 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 class LeaveCommand extends Command
{ {
public LeaveCommand(Chords bot) public LeaveCommand(Main bot)
{ {
super(bot, "leave"); super(bot, "leave");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
if (bot.getCurrentVoiceChannel() != null) if (bot.getCurrentVoiceChannel() != null)
{ {

@ -21,10 +21,10 @@ import java.net.URL;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import moe.nekojimi.chords.Chords; import moe.nekojimi.chords.Main;
import moe.nekojimi.chords.TrackRequest;
import moe.nekojimi.musicsearcher.Query; import moe.nekojimi.musicsearcher.Query;
import moe.nekojimi.musicsearcher.Result; import moe.nekojimi.musicsearcher.Result;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
/** /**
* *
@ -37,21 +37,18 @@ public class PlayCommand extends Command
private List<Result> lastSearchResults; private List<Result> lastSearchResults;
public PlayCommand(Chords main) public PlayCommand(Main main)
{ {
super(main, "play"); super(main, "play");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
TrackRequest request = new TrackRequest();
request.setInvocation(invocation);
try try
{ {
final URL url = new URL(invocation.getArgs().get(0)); final URL url = new URL(arg.get(0));
request.setUrl(url); bot.queueDownload(url, event);
bot.queueDownload(request);
} catch (MalformedURLException mux) } catch (MalformedURLException mux)
{ {
@ -60,18 +57,17 @@ public class PlayCommand extends Command
{ {
try try
{ {
int index = Integer.parseInt(invocation.getArgs().get(0)); int index = Integer.parseInt(arg.get(0));
int size = lastSearchResults.size(); int size = lastSearchResults.size();
if (index >= 1 && index <= size) if (index >= 1 && index <= size)
{ {
Result result = lastSearchResults.get(index - 1); Result res = lastSearchResults.get(index - 1);
request.setResult(result); bot.queueDownload(res, event);
bot.queueDownload(request); // event.getChannel().sendMessage("Song removed.").queue();
// event.getChannel().sendMessage("Track removed.").queue();
} else if (size > 1) } 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) 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; return;
} catch (NumberFormatException nfx) } catch (NumberFormatException nfx)
@ -81,13 +77,13 @@ public class PlayCommand extends Command
} }
// otherwise, try searching // otherwise, try searching
CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(invocation.getArgs().stream().reduce((t, u) -> t + " " + u).get())); CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(arg.stream().reduce((t, u) -> t + " " + u).get()));
invocation.respond("Searching for \"" + invocation.getArgs() + "\" ..."); event.getChannel().sendMessage("Searching for \"" + arg + "\" ...").queue();
search.orTimeout(30, TimeUnit.SECONDS).whenCompleteAsync((List<Result> results, Throwable exec) -> search.orTimeout(30, TimeUnit.SECONDS).whenCompleteAsync((List<Result> results, Throwable exec) ->
{ {
if (exec != null) if (exec != null)
{ {
invocation.respond("Failed to search! Reason: " + exec.getMessage()); event.getChannel().sendMessage("Failed to search! Reason: " + exec.getMessage()).queue();
return; return;
} }
@ -95,14 +91,13 @@ public class PlayCommand extends Command
if (results.isEmpty()) if (results.isEmpty())
{ {
invocation.respond("Found nothing! :("); event.getChannel().sendMessage("Found nothing! :(").queue();
return; return;
} }
if (results.get(0).getScore() >= SEARCH_SCORE_THRESHOLD_AUTOPLAY) if (results.get(0).getScore() >= SEARCH_SCORE_THRESHOLD_AUTOPLAY)
{ {
request.setResult(results.get(0)); bot.queueDownload(results.get(0).getLink(), event);
bot.queueDownload(request);
return; return;
} }
@ -123,15 +118,10 @@ public class PlayCommand extends Command
i++; i++;
} }
resultString += "Type eg. `!play 1` to select"; 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(); // 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.List;
import java.util.Queue; import java.util.Queue;
import moe.nekojimi.chords.Chords; import moe.nekojimi.chords.Downloader;
import moe.nekojimi.chords.Track; import moe.nekojimi.chords.Main;
import moe.nekojimi.chords.TrackRequest; import moe.nekojimi.chords.Song;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
public class QueueCommand extends Command public class QueueCommand extends Command
{ {
public QueueCommand(Chords bot) public QueueCommand(Main bot)
{ {
super(bot, "queue"); super(bot, "queue");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
String message = ">>> "; String message = ">>> ";
int i = 1; int i = 1;
Track currentTrack = null; final Song currentSong = bot.getMusicHandler().getCurrentSong();
if (bot.getMusicHandler() != null) if (currentSong != null)
currentTrack = bot.getMusicHandler().getCurrentTrack(); message += ":notes: **Now playing: " + currentSong + "**\n";
if (currentTrack != null)
message += ":notes: **Now playing: " + currentTrack + "**\n";
else else
message += ":mute: **Not playing anything right now.**\n"; message += ":mute: **Not playing anything right now.**\n";
final Queue<Track> trackQueue = bot.getQueueManager().getJukeboxQueue(); final Queue<Song> songQueue = bot.getMusicHandler().getSongQueue();
if (!trackQueue.isEmpty()) if (!songQueue.isEmpty())
{ {
message += "__Ready to play:__\n"; message += "__Ready to play:__\n";
for (Track track : trackQueue) for (Song song : songQueue)
{ {
message += ":bread: **" + i + ":** " + track + "\n"; message += ":bread: **" + (i) + ":** " + song + "\n";
i++; i++;
} }
} }
final List<TrackRequest> downloading = bot.getDownloader().getDownloadingTracks(); final List<Downloader.DownloadTask> downloadQueue = bot.getDownloader().getDownloadQueue();
if (!downloading.isEmpty())
{
message += "__Downloading:__\n";
for (TrackRequest request : downloading)
{
message += ":satellite: **" + (i) + ":** " + request + "\n";
i++;
}
}
final List<TrackRequest> downloadQueue = bot.getDownloader().getDownloadQueue();
if (!downloadQueue.isEmpty()) if (!downloadQueue.isEmpty())
{ {
message += "__In queue for download:__\n"; message += "__Downloading:__\n";
for (TrackRequest request : downloadQueue) for (Downloader.DownloadTask task : downloadQueue)
{ {
message += ":inbox_tray: **" + (i) + ":** " + request + "\n"; message += ":inbox_tray: **" + (i) + ":** " + task.getSong() + "\n";
i++; i++;
} }
} }
if (downloading.isEmpty() && trackQueue.isEmpty() && downloadQueue.isEmpty()) if (downloadQueue.isEmpty() && songQueue.isEmpty())
message += ":mailbox_with_no_mail: The track queue is empty."; message += ":mailbox_with_no_mail: The track queue is empty.";
// :inbox_tray: // :inbox_tray:
invocation.respond(message); event.getChannel().sendMessage(message).queue();
} }
} }

@ -16,34 +16,36 @@
*/ */
package moe.nekojimi.chords.commands; 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 class RemoveCommand extends Command
{ {
public RemoveCommand(Chords bot) public RemoveCommand(Main bot)
{ {
super(bot, "remove"); super(bot, "remove");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
try try
{ {
int i = Integer.parseInt(invocation.getArgs().get(0)); int i = Integer.parseInt(arg.get(0));
boolean removed = bot.getQueueManager().removeTrack(i - 1); boolean removed = bot.getMusicHandler().removeSong(i - 1);
final int size = bot.getQueueManager().getJukeboxQueue().size(); final int size = bot.getMusicHandler().getSongQueue().size();
if (removed) if (removed)
invocation.respond("Track removed."); event.getChannel().sendMessage("Song removed.").queue();
else if (size > 1) 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) 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) } 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; 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 class RestartCommand extends Command
{ {
public RestartCommand(Chords bot) public RestartCommand(Main bot)
{ {
super(bot, "restart"); super(bot, "restart");
} }
@Override @Override
public void call(Invocation invocation) public void call(GuildMessageReceivedEvent event, List<String> arg)
{ {
// TODO: this needs to clear the current data queue // TODO: this needs to clear the current data queue
boolean ok = bot.getQueueManager().restartTrack(); boolean ok = bot.getMusicHandler().restartSong();
if (ok) if (ok)
invocation.respond("Restarted current track!"); event.getChannel().sendMessage("Restarted current song!").queue();
else else
invocation.respond("Cannot restart!"); event.getChannel().sendMessage("Cannot restart!").queue();
} }
} }

@ -16,24 +16,26 @@
*/ */
package moe.nekojimi.chords.commands; 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 class SkipCommand extends Command
{ {
public SkipCommand(Chords bot) public SkipCommand(Main bot)
{ {
super(bot, "skip"); super(bot, "skip");
} }
@Override @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) if (ok)
invocation.respond("Skipped to next track!"); event.getChannel().sendMessage("Skipped to next song!").queue();
else else
invocation.respond("There's no more tracks!"); event.getChannel().sendMessage("There's no more songs!").queue();
} }
} }

Loading…
Cancel
Save