Compare commits
55 Commits
Author | SHA1 | Date |
---|---|---|
|
5ec62d4d43 | |
|
a9c5bb37f8 | |
|
6759afe76b | |
|
c8684dbe75 | |
|
11c8991943 | |
|
95d2e41a03 | |
|
536743c6c1 | |
|
143f5dacda | |
|
758f6a0d48 | |
|
a901163af3 | |
|
abd676fa49 | |
|
dd977cc899 | |
|
1d78f2a3fb | |
|
c2c748470f | |
|
268af0ff3f | |
|
26010946c2 | |
|
ab26e65fdb | |
|
571139a59c | |
|
95f838c392 | |
|
7d3795b10a | |
|
3899792e7f | |
|
fdbd8937f5 | |
|
b41f5c97a3 | |
|
1ad6617914 | |
|
16c2c61e76 | |
|
2a25e1368d | |
|
abfab9a18d | |
|
07b45309c1 | |
|
310158a39b | |
|
de807b4524 | |
|
d9a7da27f2 | |
|
be6036cd5d | |
|
c7ab883d5e | |
|
3ff544d699 | |
|
9fa5e36cdc | |
|
d50657ef90 | |
|
f1cc31dcd9 | |
|
0914cadaf5 | |
|
490101fcad | |
|
1dba55620f | |
|
0c29d56317 | |
|
6ef49c9e51 | |
|
7c4ea13e3c | |
|
7cd118985a | |
|
2ec6da49b5 | |
|
f043ab906d | |
|
6f23b60511 | |
|
1b7d4adeb9 | |
|
000e0e5d5c | |
|
52e34493bd | |
|
824d08caad | |
|
b0415b0324 | |
|
ae7fe8cc7b | |
|
4e654d2d18 | |
|
ca3ebd712b |
|
@ -55,5 +55,9 @@ dist/
|
|||
nbdist/
|
||||
.nb-gradle/
|
||||
|
||||
# Git probably
|
||||
*.orig
|
||||
|
||||
# Secrets!
|
||||
secrets.yml
|
||||
settings.yml
|
||||
|
|
11
README.md
11
README.md
|
@ -1,2 +1,11 @@
|
|||
# Chords
|
||||
# Chords - Yet Another Discord Music Bot
|
||||
|
||||

|
||||
|
||||
Basically everyone has written one at this point, but our little community needed one, so I made one!
|
||||
|
||||
Here's some stuff about it:
|
||||
|
||||
- Can play from any site that youtube-dl supports ([that's over 1800!](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md))
|
||||
- Built-in multi-site searching via my own [MusicSearcher](https://fluff.nekojimi.moe/gitea/Nekojimi/MusicSearcher) project
|
||||
- More cool stuff coming soon!
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
@ -14,5 +14,6 @@ That way multiple projects can share the same settings (useful for formatting ru
|
|||
Any value defined here will override the pom.xml file value but is only applicable to the current project.
|
||||
-->
|
||||
<netbeans.hint.license>gpl30</netbeans.hint.license>
|
||||
<org-netbeans-modules-javascript2-requirejs.enabled>true</org-netbeans-modules-javascript2-requirejs.enabled>
|
||||
</properties>
|
||||
</project-shared-configuration>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
|
||||
</goals>
|
||||
<properties>
|
||||
<exec.args>-classpath %classpath moe.nekojimi.chords.Main ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.executable>java</exec.executable>
|
||||
</properties>
|
||||
</action>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
|
||||
</goals>
|
||||
<properties>
|
||||
<exec.args>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath moe.nekojimi.chords.Main ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.args>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.executable>java</exec.executable>
|
||||
<jpda.listen>true</jpda.listen>
|
||||
</properties>
|
||||
|
@ -39,7 +39,7 @@
|
|||
<goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
|
||||
</goals>
|
||||
<properties>
|
||||
<exec.args>-classpath %classpath ${packageClassName} ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.args>-classpath %classpath moe.nekojimi.chords.Main -token ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk</exec.args>
|
||||
<exec.executable>java</exec.executable>
|
||||
</properties>
|
||||
</action>
|
||||
|
|
9
pom.xml
9
pom.xml
|
@ -20,7 +20,7 @@
|
|||
<archive>
|
||||
<manifest>
|
||||
<mainClass>
|
||||
moe.nekojimi.chords.Main
|
||||
moe.nekojimi.chords.Chords
|
||||
</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
|
@ -35,7 +35,7 @@
|
|||
<dependency>
|
||||
<groupId>net.dv8tion</groupId>
|
||||
<artifactId>JDA</artifactId>
|
||||
<version>4.3.0_277</version>
|
||||
<version>5.0.0-beta.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
|
@ -54,6 +54,11 @@
|
|||
<version>1.1.4</version>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.beust</groupId>
|
||||
<artifactId>jcommander</artifactId>
|
||||
<version>1.78</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
ytdl-cmd: /usr/bin/yt-dlp
|
||||
discord-token: ODkwNjU5MjI2NDE2OTg0MDY0.YUzBCw.jHZWpIZSYeaYA7Sc7h93W_jV-rk
|
||||
local-addr: 192.168.1.84
|
|
@ -0,0 +1,556 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import com.amihaiemil.eoyaml.Yaml;
|
||||
import com.amihaiemil.eoyaml.YamlMapping;
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.neovisionaries.ws.client.WebSocketFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.security.auth.login.LoginException;
|
||||
import moe.nekojimi.chords.commands.*;
|
||||
import moe.nekojimi.musicsearcher.providers.MetaSearcher;
|
||||
import moe.nekojimi.musicsearcher.providers.Searcher;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.entities.*;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
|
||||
import net.dv8tion.jda.api.events.message.MessageEmbedEvent;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.managers.AudioManager;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import net.dv8tion.jda.api.utils.Compression;
|
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||
import net.dv8tion.jda.internal.utils.IOUtil;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* The Chords music Discord bot.
|
||||
*
|
||||
* @author Nekojimi
|
||||
*/
|
||||
public final class Chords extends ListenerAdapter
|
||||
{
|
||||
|
||||
private static final CommandOptions options = new CommandOptions();
|
||||
private static Settings settings;
|
||||
|
||||
private final File dataDirectory;
|
||||
private final File playlistsDirectory;
|
||||
|
||||
private MusicHandler musicHandler;
|
||||
private final Downloader downloader;
|
||||
private final Searcher searcher;
|
||||
private final QueueManager queueManager;
|
||||
// private Recommender recommender;
|
||||
private JDA jda;
|
||||
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
private final Command helpCommand;
|
||||
private PlayCommand playCommand;
|
||||
|
||||
private AudioChannel currentVoiceChannel = null;
|
||||
|
||||
private final Map<String, Playlist> playlists = new HashMap<>();
|
||||
|
||||
/**
|
||||
* @param args the command line arguments
|
||||
*/
|
||||
public static void main(String[] args) throws LoginException, IOException
|
||||
{
|
||||
|
||||
JCommander commander = JCommander.newBuilder()
|
||||
.args(args)
|
||||
.acceptUnknownOptions(true)
|
||||
.addObject(options)
|
||||
.build();
|
||||
|
||||
// We only need 2 gateway intents enabled for this example:
|
||||
EnumSet<GatewayIntent> intents = EnumSet.of(
|
||||
// We need messages in guilds to accept commands from users
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
// We need voice states to connect to the voice channel
|
||||
GatewayIntent.GUILD_VOICE_STATES,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.MESSAGE_CONTENT
|
||||
);
|
||||
|
||||
settings = new Settings(new File(options.getSettingsPath()));
|
||||
|
||||
JDABuilder builder = JDABuilder.createDefault(settings.getDiscordToken(), intents);
|
||||
|
||||
builder.enableIntents(GatewayIntent.MESSAGE_CONTENT);
|
||||
if (settings.getLocalAddr() != null)
|
||||
{
|
||||
System.out.println("Using local address: " + settings.getLocalAddr());
|
||||
// make local binding socket factory
|
||||
final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory();
|
||||
localBindSocketFactory.setLocalAddress(InetAddress.getByName(settings.getLocalAddr()));
|
||||
|
||||
// install local socket factory for HTTP
|
||||
OkHttpClient.Builder httpBuilder = IOUtil.newHttpClientBuilder();
|
||||
httpBuilder.socketFactory(localBindSocketFactory);
|
||||
builder.setHttpClientBuilder(httpBuilder);
|
||||
|
||||
// install local socket factory for websockets
|
||||
final WebSocketFactory webSocketFactory = new WebSocketFactory();
|
||||
webSocketFactory.setSocketFactory(localBindSocketFactory);
|
||||
builder.setWebsocketFactory(webSocketFactory);
|
||||
}
|
||||
|
||||
// Disable parts of the cache
|
||||
builder.disableCache(CacheFlag.MEMBER_OVERRIDES);
|
||||
// Enable the bulk delete event
|
||||
builder.setBulkDeleteSplittingEnabled(false);
|
||||
// Disable compression (not recommended)
|
||||
builder.setCompression(Compression.NONE);
|
||||
// Set activity (like "playing Something")
|
||||
builder.setActivity(Activity.playing("music!"));
|
||||
|
||||
final Chords listener = new Chords();
|
||||
|
||||
builder.addEventListeners(listener);
|
||||
builder.setAutoReconnect(true);
|
||||
|
||||
JDA jda = builder.build();
|
||||
listener.setJda(jda);
|
||||
}
|
||||
private final Consumer<Track> nowPlayingConsumer = new NowPlayingConsumer();
|
||||
|
||||
private final BiConsumer<TrackRequest, Exception> downloaderMessageHandler = new DownloaderMessageHandler();
|
||||
|
||||
public Chords() throws IOException
|
||||
{
|
||||
log("INFO", "Starting up...");
|
||||
|
||||
// init dirs
|
||||
dataDirectory = new File(System.getProperty("user.dir"));
|
||||
playlistsDirectory = initDirectory(dataDirectory, "playlists");
|
||||
|
||||
// init downloader
|
||||
downloader = new Downloader();
|
||||
downloader.setMessageHandler(downloaderMessageHandler);
|
||||
|
||||
// init searcher
|
||||
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
|
||||
|
||||
// init queue manager
|
||||
queueManager = new QueueManager();
|
||||
queueManager.addSource(downloader);
|
||||
|
||||
// init commands
|
||||
addCommand(new JoinCommand(this));
|
||||
addCommand(new LeaveCommand(this));
|
||||
playCommand = new PlayCommand(this);
|
||||
addCommand(playCommand);
|
||||
addCommand(new QueueCommand(this));
|
||||
addCommand(new RemoveCommand(this));
|
||||
addCommand(new RestartCommand(this));
|
||||
addCommand(new SkipCommand(this));
|
||||
helpCommand = new HelpCommand(this);
|
||||
addCommand(helpCommand);
|
||||
|
||||
// load playlists
|
||||
loadPlaylists();
|
||||
|
||||
log("INFO", "Started OK!");
|
||||
}
|
||||
|
||||
private void addCommand(Command command)
|
||||
{
|
||||
commands.put(command.getKeyword(), command);
|
||||
}
|
||||
|
||||
public void setJda(JDA jda)
|
||||
{
|
||||
this.jda = jda;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event)
|
||||
{
|
||||
super.onGuildVoiceUpdate(event);
|
||||
if (this.currentVoiceChannel == null)
|
||||
return;
|
||||
|
||||
final AudioChannel channelLeft = event.getChannelLeft();
|
||||
if (channelLeft != null && channelLeft == currentVoiceChannel)
|
||||
{
|
||||
if (channelLeft.getMembers().size() <= 1) // just us now
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onMessageReceived(MessageReceivedEvent event)
|
||||
{
|
||||
super.onMessageReceived(event); //To change body of generated methods, choose Tools | Templates.
|
||||
Message message = event.getMessage();
|
||||
User author = message.getAuthor();
|
||||
String content = message.getContentRaw();
|
||||
Guild guild = event.getGuild();
|
||||
|
||||
// Ignore message if it's not in a music channel
|
||||
if (!event.getChannel().getName().contains("music"))
|
||||
return;
|
||||
|
||||
// Ignore message if bot
|
||||
if (author.isBot())
|
||||
return;
|
||||
|
||||
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content);
|
||||
|
||||
Invocation invocation = null;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
URL parseURL = new URL(content.trim());
|
||||
invocation = new Invocation(event, List.of(parseURL.toExternalForm()));
|
||||
playCommand.call(invocation);
|
||||
return;
|
||||
} catch (MalformedURLException ex)
|
||||
{
|
||||
// not a URL, then
|
||||
}
|
||||
|
||||
String[] split = content.split("\\s+");
|
||||
List<String> args = new ArrayList<>();
|
||||
Collections.addAll(args, split);
|
||||
args.remove(0);
|
||||
|
||||
invocation = new Invocation(event, args);
|
||||
invocation.setRequestMessage(message);
|
||||
|
||||
String cmd = split[0].toLowerCase();
|
||||
|
||||
if (cmd.startsWith("!"))
|
||||
{
|
||||
cmd = cmd.substring(1); // strip prefix char
|
||||
|
||||
if (commands.containsKey(cmd))
|
||||
{
|
||||
Command command = commands.get(cmd);
|
||||
command.call(invocation);
|
||||
} else
|
||||
{
|
||||
helpCommand.call(invocation);
|
||||
}
|
||||
} else if (!event.getMessage().getAttachments().isEmpty())
|
||||
{
|
||||
for (Message.Attachment attach : event.getMessage().getAttachments())
|
||||
{
|
||||
if (attach.getContentType().startsWith("audio") || attach.getContentType().startsWith("video"))
|
||||
{
|
||||
try
|
||||
{
|
||||
invocation = new Invocation(event, List.of(new URL(attach.getUrl()).toExternalForm()));
|
||||
invocation.setRequestMessage(message);
|
||||
playCommand.call(invocation);
|
||||
} catch (MalformedURLException ex)
|
||||
{
|
||||
Logger.getLogger(Chords.class.getName()).log(Level.WARNING, null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
if (invocation != null)
|
||||
invocation.respond("Error: " + ex.getMessage());
|
||||
else
|
||||
event.getChannel().sendMessage("Error: " + ex.getMessage()).queue();
|
||||
log("UERR", "Command error:" + ex.getMessage());
|
||||
}
|
||||
|
||||
// TODO: this will handle uploading files, maybe
|
||||
}
|
||||
|
||||
public void queueDownload(TrackRequest request)
|
||||
{
|
||||
request.getInvocation().respond("Request pending...");
|
||||
downloader.accept(request);
|
||||
}
|
||||
|
||||
public void setStatus(Track nowPlaying)
|
||||
{
|
||||
jda.getPresence().setActivity(Activity.listening(nowPlaying.toString()));
|
||||
}
|
||||
|
||||
public void clearStatus()
|
||||
{
|
||||
jda.getPresence().setActivity(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to requested channel and start echo handler
|
||||
*
|
||||
* @param channel
|
||||
* The channel to connect to
|
||||
*/
|
||||
public void connectTo(AudioChannel channel)
|
||||
{
|
||||
Guild guild = channel.getGuild();
|
||||
// Get an audio manager for this guild, this will be created upon first use for each guild
|
||||
AudioManager audioManager = guild.getAudioManager();
|
||||
musicHandler = new MusicHandler();
|
||||
queueManager.setHandler(musicHandler);
|
||||
// Create our Send/Receive handler for the audio connection
|
||||
// EchoHandler handler = new EchoHandler();
|
||||
|
||||
// The order of the following instructions does not matter!
|
||||
// Set the sending handler to our echo system
|
||||
audioManager.setSendingHandler(musicHandler);
|
||||
// Set the receiving handler to the same echo system, otherwise we can't echo anything
|
||||
// audioManager.setReceivingHandler(handler);
|
||||
// Connect to the voice channel
|
||||
audioManager.openAudioConnection(channel);
|
||||
currentVoiceChannel = channel;
|
||||
|
||||
musicHandler.setNowPlayingConsumer(nowPlayingConsumer);
|
||||
}
|
||||
|
||||
public void disconnect()
|
||||
{
|
||||
if (currentVoiceChannel != null)
|
||||
{
|
||||
Guild guild = currentVoiceChannel.getGuild();
|
||||
AudioManager audioManager = guild.getAudioManager();
|
||||
audioManager.setSendingHandler(null);
|
||||
audioManager.closeAudioConnection();
|
||||
musicHandler = null;
|
||||
currentVoiceChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void log(String type, String message)
|
||||
{
|
||||
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message);
|
||||
}
|
||||
|
||||
public File initDirectory(File parent, String name) throws IOException
|
||||
{
|
||||
File ret = new File(parent, name);
|
||||
if (!ret.exists())
|
||||
Files.createDirectories(ret.toPath());
|
||||
if (!ret.canRead())
|
||||
throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!");
|
||||
if (!ret.canWrite())
|
||||
throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!");
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void loadPlaylists()
|
||||
{
|
||||
File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml"));
|
||||
for (File file : files)
|
||||
{
|
||||
try
|
||||
{
|
||||
YamlMapping map = Yaml.createYamlInput(file).readYamlMapping();
|
||||
Playlist playlist = Playlist.fromYaml(map);
|
||||
playlists.put(playlist.getName(), playlist);
|
||||
} catch (IOException ex)
|
||||
{
|
||||
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setRecommenderEnabled(boolean enabled)
|
||||
{
|
||||
// if (recommender == null && enabled)
|
||||
// {
|
||||
//// recommender = new Recommender();
|
||||
// downloader.addSource(recommender);
|
||||
// } else if (recommender != null && !enabled)
|
||||
// {
|
||||
//// downloader.removeSource(recommender);
|
||||
// recommender = null;
|
||||
// }
|
||||
}
|
||||
|
||||
public MusicHandler getMusicHandler()
|
||||
{
|
||||
return musicHandler;
|
||||
}
|
||||
|
||||
public Downloader getDownloader()
|
||||
{
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public Searcher getSearcher()
|
||||
{
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public JDA getJda()
|
||||
{
|
||||
return jda;
|
||||
}
|
||||
|
||||
public AudioChannel getCurrentVoiceChannel()
|
||||
{
|
||||
return currentVoiceChannel;
|
||||
}
|
||||
|
||||
public QueueManager getQueueManager()
|
||||
{
|
||||
return queueManager;
|
||||
}
|
||||
|
||||
public static Settings getSettings()
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static CommandOptions getOptions()
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
public File getDataDirectory()
|
||||
{
|
||||
return dataDirectory;
|
||||
}
|
||||
|
||||
public File getPlaylistsDirectory()
|
||||
{
|
||||
return playlistsDirectory;
|
||||
}
|
||||
|
||||
public Map<String, Command> getCommands()
|
||||
{
|
||||
return commands;
|
||||
}
|
||||
|
||||
public Map<String, Playlist> getPlaylists()
|
||||
{
|
||||
return playlists;
|
||||
}
|
||||
|
||||
// public Recommender getRecommender()
|
||||
// {
|
||||
// return recommender;
|
||||
// }
|
||||
|
||||
private class DownloaderMessageHandler implements BiConsumer<TrackRequest, Exception>
|
||||
{
|
||||
|
||||
private static final String PROGRESS_SYMBOLS = " ▏▎▍▌▋▊▉█";
|
||||
|
||||
private double lastProgressUpdate = 0.0;
|
||||
private boolean initialUpdateSent = false;
|
||||
|
||||
/*
|
||||
⠀⡀⣀⣠⣤⣦⣶⣾⣿
|
||||
🌑🌘🌗🌖🌕
|
||||
🌩🌨🌧🌦🌥🌤🌞
|
||||
○◔◑◕●
|
||||
□🞎🞏🞐🞑🞒🞓■
|
||||
□▤▥▦▧▨▩■
|
||||
▁▂▃▄▅▆▇█
|
||||
*/
|
||||
// private static final String PROGRESS_SYMBOLS = " ⠀⡀⣀⣠⣤⣦⣶⣾⣿";
|
||||
|
||||
public DownloaderMessageHandler()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(TrackRequest request, Exception ex)
|
||||
{
|
||||
boolean shouldUpdate = false;
|
||||
String response = "";
|
||||
if (request.getTracks().size() > 1)
|
||||
response += "Downloading " + request.getTracks().size() + " tracks:\n";
|
||||
for (Track track : request.getTracks())
|
||||
{
|
||||
// TextChannel channel = jda.getTextChannelById(track.getRequestedIn());
|
||||
// String bracketNo = "[" + track.getNumber() + "] ";
|
||||
if (ex == null)
|
||||
if (track.getLocation() != null && track.getProgress() >= 100)
|
||||
{
|
||||
response += ("Finished downloading " + track + "!");
|
||||
log("DOWN", "Downloaded " + track);
|
||||
shouldUpdate = true;
|
||||
} else
|
||||
{
|
||||
Format format = track.getBestFormat();
|
||||
String formatDetails = "";
|
||||
if (format != null)
|
||||
{
|
||||
final int bitrate = format.getSampleRate() / 1000;
|
||||
final long size = format.getSize();
|
||||
String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB";
|
||||
String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k";
|
||||
formatDetails = " (" + bitFmt + ", " + sizeFmt + ")";
|
||||
}
|
||||
|
||||
String progressDetails = "";
|
||||
if (track.getProgress() >= 0)
|
||||
{
|
||||
if (track.getProgress() >= lastProgressUpdate + 5.0)
|
||||
{
|
||||
shouldUpdate = true;
|
||||
lastProgressUpdate = track.getProgress();
|
||||
}
|
||||
progressDetails = " [" + String.format("%.1f", track.getProgress()) + "%]";
|
||||
}
|
||||
|
||||
response += ("Now downloading " + track + formatDetails + progressDetails + " ...");
|
||||
log("DOWN", "Downloading " + track + "...");
|
||||
}
|
||||
else
|
||||
{
|
||||
response += ("Failed to download " + track + "! Reason: " + ex.getMessage());
|
||||
log("DOWN", "Failed to download " + track + "! Reason: " + ex.getMessage());
|
||||
shouldUpdate = true;
|
||||
}
|
||||
response += "\n";
|
||||
}
|
||||
if (!response.isEmpty() && (shouldUpdate || !initialUpdateSent))
|
||||
{
|
||||
request.getInvocation().respond(response);
|
||||
initialUpdateSent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NowPlayingConsumer implements Consumer<Track>
|
||||
{
|
||||
|
||||
public NowPlayingConsumer()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Track track)
|
||||
{
|
||||
if (track != null)
|
||||
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, track.toString()));
|
||||
else
|
||||
jda.getPresence().setActivity(null);
|
||||
|
||||
// if (recommender != null)
|
||||
// recommender.addSeed(track);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (C) 2022 jimj316
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class CommandOptions
|
||||
{
|
||||
|
||||
@Parameter(names = "-settings", description = "A path to the settings.yml file.")
|
||||
private String settingsPath = "settings.yml";
|
||||
|
||||
// @Parameter(names = "-token", description = "The API token for Discord.", required = true)
|
||||
// private String token;
|
||||
//
|
||||
// @Parameter(names = "-local-addr", description = "The local address to bind to.")
|
||||
// private String localAddress;
|
||||
|
||||
// public String getToken()
|
||||
// {
|
||||
// return token;
|
||||
// }
|
||||
//
|
||||
// public String getLocalAddress()
|
||||
// {
|
||||
// return localAddress;
|
||||
// }
|
||||
|
||||
public String getSettingsPath()
|
||||
{
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
}
|
|
@ -5,108 +5,134 @@
|
|||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import com.beust.jcommander.Strings;
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonArray;
|
||||
import javax.json.JsonObject;
|
||||
import javax.json.JsonReader;
|
||||
import moe.nekojimi.chords.Downloader.DownloadTask;
|
||||
import moe.nekojimi.musicsearcher.Result;
|
||||
import net.dv8tion.jda.api.audio.AudioSendHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class Downloader implements Consumer<DownloadTask>
|
||||
public class Downloader extends QueueThing<TrackRequest, Track>
|
||||
{
|
||||
|
||||
private static final int BITRATE_TARGET = 64_000;
|
||||
private static final int DOWNLOAD_TIMEOUT = 300;
|
||||
private static final int INFO_TIMEOUT = 60;
|
||||
private static final int FORMAT_TIMEOUT = 5;
|
||||
|
||||
private static final int BITRATE_TARGET = (int) AudioSendHandler.INPUT_FORMAT.getSampleRate();
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("^([\\w]+)\\s+([\\w]+)\\s+(\\w+ ?\\w*)\\s+(.*)$");
|
||||
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+)");
|
||||
|
||||
private final List<DownloadTask> downloadQueue = new LinkedList<>();
|
||||
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 LinkedBlockingDeque<Runnable> workQueue = new LinkedBlockingDeque<>();
|
||||
private final ThreadPoolExecutor exec = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue);
|
||||
// private Consumer<Song> next;
|
||||
private BiConsumer<SongRequest, Exception> messageHandler;
|
||||
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 30, TimeUnit.SECONDS, workQueue);
|
||||
private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
|
||||
// private Consumer<Track> next;
|
||||
private BiConsumer<TrackRequest, Exception> messageHandler;
|
||||
|
||||
private File downloadDir = null;
|
||||
|
||||
private int trackNumber = 1;
|
||||
|
||||
public Downloader()
|
||||
{
|
||||
|
||||
super(new LinkedList<>());
|
||||
}
|
||||
//
|
||||
// @Override
|
||||
// public void accept(TrackRequest request)
|
||||
// {
|
||||
// if all tracks of the request are already downloaded, just skip
|
||||
// if (!request.getTracks().isEmpty() && request.getTracks().stream().allMatch((t) -> t.isDownloaded()))
|
||||
// {
|
||||
// for (Track track : request.getTracks())
|
||||
// getDestination().accept(track);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// inputQueue.add(task);
|
||||
|
||||
// }
|
||||
|
||||
@Override
|
||||
public void accept(DownloadTask task)
|
||||
protected boolean completePromise(Promise<TrackRequest, Track> promise)
|
||||
{
|
||||
// if already downloaded, just skip
|
||||
Song song = task.request.getSong();
|
||||
if (song.isDownloaded())
|
||||
{
|
||||
task.getDestination().accept(song);
|
||||
return;
|
||||
}
|
||||
TrackRequest request = promise.getInput();
|
||||
if (request == null)
|
||||
return false;
|
||||
|
||||
downloadQueue.add(task);
|
||||
getInfo(song);
|
||||
exec.submit(() ->
|
||||
executor.submit(() ->
|
||||
{
|
||||
downloadingTracks.add(request);
|
||||
try
|
||||
{
|
||||
getFormats(song);
|
||||
download(task);
|
||||
|
||||
getInfo(request);
|
||||
// boolean streamOutput = request.getTracks().size() == 1;
|
||||
boolean streamOutput = false;
|
||||
download(promise, streamOutput);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
}
|
||||
downloadingTracks.remove(request);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void chooseFormats(Song song)
|
||||
private List<Format> sortFormats(Collection<Format> input)
|
||||
{
|
||||
List<Format> formats = song.getFormats();
|
||||
if (formats.isEmpty())
|
||||
return;
|
||||
// System.out.println("Choosing from " + formats.size() + " formats:");
|
||||
// System.out.println(formats);
|
||||
List<Format> formats = new ArrayList<>(input);
|
||||
formats.sort((Format a, Format b) ->
|
||||
{
|
||||
// audio only preferred to video
|
||||
// System.out.println("sort entered; a=" + a.toString() + " b=" + b.toString());
|
||||
int comp = 0;
|
||||
comp = Boolean.compare(a.isAudioOnly(), b.isAudioOnly());
|
||||
// System.out.println("\tCompared on audio only: " + comp);
|
||||
if (comp == 0)
|
||||
{
|
||||
// known preferred to unknown
|
||||
if (a.getBitrate() == b.getBitrate())
|
||||
if (a.getSampleRate() == b.getSampleRate())
|
||||
comp = 0;
|
||||
else if (a.getBitrate() <= 0)
|
||||
else if (a.getSampleRate() <= 0)
|
||||
comp = 1;
|
||||
else if (b.getBitrate() <= 0)
|
||||
else if (b.getSampleRate() <= 0)
|
||||
comp = -1;
|
||||
else // closer to the target bitrate is best
|
||||
{
|
||||
int aDist = Math.abs(a.getBitrate() - BITRATE_TARGET);
|
||||
int bDist = Math.abs(b.getBitrate() - BITRATE_TARGET);
|
||||
int aDist = Math.abs(a.getSampleRate() - BITRATE_TARGET);
|
||||
int bDist = Math.abs(b.getSampleRate() - BITRATE_TARGET);
|
||||
comp = Integer.compare(bDist, aDist);
|
||||
// System.out.println("\tCompared on bitrate distance: " + comp);
|
||||
}
|
||||
}
|
||||
if (comp == 0)
|
||||
|
@ -114,53 +140,85 @@ public class Downloader implements Consumer<DownloadTask>
|
|||
// known preferred to unknown
|
||||
if (a.getSize() == b.getSize())
|
||||
comp = 0;
|
||||
if (a.getSize() <= 0)
|
||||
else if (a.getSize() <= 0)
|
||||
comp = 1;
|
||||
else if (b.getSize() <= 0)
|
||||
comp = -1;
|
||||
else // smaller is better
|
||||
{
|
||||
comp = Long.compare(b.getSize(), a.getSize());
|
||||
// System.out.println("\tCompared on filesize: " + comp);
|
||||
}
|
||||
}
|
||||
// System.out.println("\tOverall: " + comp);
|
||||
return -comp;
|
||||
});
|
||||
song.setFormats(formats);
|
||||
// System.out.println("Sorting done! Formats:" + formats);
|
||||
return formats;
|
||||
}
|
||||
|
||||
private void getFormats(Song song)
|
||||
private void getFormats(Track track)
|
||||
{
|
||||
//
|
||||
if (!track.getFormats().isEmpty())
|
||||
return;
|
||||
try
|
||||
{
|
||||
String cmd = "/usr/bin/youtube-dl --skip-download -F " + song.getUrl().toString();
|
||||
Process exec = runCommand(cmd, 5);
|
||||
// String cmd = + " --skip-download -F " + track.getUrl().toString();
|
||||
Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "-F", track.getUrl().toString()), FORMAT_TIMEOUT);
|
||||
InputStream input = exec.getInputStream();
|
||||
String output = new String(input.readAllBytes(), Charset.defaultCharset());
|
||||
|
||||
List<Format> formats = new ArrayList<>();
|
||||
|
||||
int codeCol = 0;
|
||||
int extCol = 1;
|
||||
int resCol = 2;
|
||||
int noteCol = 3;
|
||||
int sizeCol = -1;
|
||||
int bitrateCol = -1;
|
||||
|
||||
List<String> list = output.lines().collect(Collectors.toList());
|
||||
int i = 0;
|
||||
while (!list.get(i).contains("Available formats"))
|
||||
i++;
|
||||
i++;
|
||||
if (list.get(i).contains("FILESIZE"))
|
||||
{
|
||||
String[] split = list.get(i).split("\\s+");
|
||||
for (int j = 0; j < split.length; j++)
|
||||
{
|
||||
switch (split[j])
|
||||
{
|
||||
case "ID":
|
||||
codeCol = j;
|
||||
break;
|
||||
case "EXT":
|
||||
extCol = j;
|
||||
break;
|
||||
case "RESOLUTION":
|
||||
resCol = j;
|
||||
break;
|
||||
case "MORE":
|
||||
noteCol = j;
|
||||
break;
|
||||
case "FILESIZE":
|
||||
sizeCol = j;
|
||||
break;
|
||||
case "TBR":
|
||||
bitrateCol = j;
|
||||
}
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
for (; i < list.size(); i++)
|
||||
{
|
||||
String line = list.get(i);
|
||||
String[] split = line.split("\\s\\s+", 4);
|
||||
String[] split = line.split("\\s+", Math.max(4, noteCol - 1));
|
||||
if (split.length < 4)
|
||||
continue;
|
||||
formats.add(new Format(split[0], split[1], split[2], split[3]));
|
||||
final Format format = new Format(split[codeCol], split[extCol], split[resCol], split[noteCol]);
|
||||
if (sizeCol >= 0)
|
||||
format.setSize(Util.parseSize(split[sizeCol]));
|
||||
if (bitrateCol >= 0)
|
||||
format.setSampleRate(Util.parseSampleRate(split[bitrateCol]));
|
||||
formats.add(format);
|
||||
}
|
||||
song.setFormats(formats);
|
||||
// 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;
|
||||
track.setFormats(formats);
|
||||
|
||||
} catch (Exception ex)
|
||||
{
|
||||
|
@ -169,23 +227,88 @@ public class Downloader implements Consumer<DownloadTask>
|
|||
}
|
||||
}
|
||||
|
||||
private void getInfo(Song song)
|
||||
private List<Track> getInfo(TrackRequest request)
|
||||
{
|
||||
List<Track> ret = new ArrayList<>();
|
||||
try
|
||||
{
|
||||
String cmd = "/usr/bin/youtube-dl --skip-download --print-json " + song.getUrl().toString();
|
||||
Process exec = runCommand(cmd, 5);
|
||||
// String cmd = Chords.getSettings().getYtdlCommand() + " --skip-download --print-json " + request.getUrl().toString();
|
||||
Process exec = runCommand(List.of(Chords.getSettings().getYtdlCommand(), "--skip-download", "--print-json", request.getUrl().toString()), INFO_TIMEOUT);
|
||||
InputStream input = exec.getInputStream();
|
||||
JsonReader reader = Json.createReader(input);
|
||||
JsonObject object = reader.readObject();
|
||||
if (song.getTitle() == null)
|
||||
song.setTitle(object.getString("title", null));
|
||||
if (song.getArtist() == null)
|
||||
song.setArtist(object.getString("uploader", null));
|
||||
|
||||
// read each line as JSON, turn each into a track object
|
||||
Scanner sc = new Scanner(input);
|
||||
while (sc.hasNextLine())
|
||||
{
|
||||
Track track = new Track(request);
|
||||
track.setNumber(trackNumber);
|
||||
trackNumber++;
|
||||
request.addTrack(track);
|
||||
|
||||
String line = sc.nextLine();
|
||||
JsonReader reader = Json.createReader(new StringReader(line));
|
||||
JsonObject object = reader.readObject();
|
||||
|
||||
Result result = request.getResult();
|
||||
|
||||
// look for metadata
|
||||
if (track.getTitle() == null)
|
||||
{
|
||||
if (result != null && !result.getTitle().isBlank())
|
||||
track.setTitle(result.getTitle());
|
||||
for (String key : INFO_TITLE_KEYS)
|
||||
{
|
||||
if (object.containsKey(key) && !object.getString(key).isBlank())
|
||||
{
|
||||
track.setTitle(object.getString(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (track.getArtist() == null)
|
||||
{
|
||||
if (result != null && !result.getArtist().isBlank())
|
||||
track.setArtist(result.getArtist());
|
||||
for (String key : INFO_ARTIST_KEYS)
|
||||
{
|
||||
if (object.containsKey(key) && !object.getString(key).isBlank())
|
||||
{
|
||||
track.setArtist(object.getString(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (track.getTitle().contains("-"))
|
||||
{
|
||||
String[] split = track.getTitle().split("-", 2);
|
||||
track.setArtist(split[0]);
|
||||
track.setTitle(split[1]);
|
||||
}
|
||||
if (track.getArtist().contains(" - Topic"))
|
||||
{
|
||||
track.setArtist(track.getArtist().replace(" - Topic", ""));
|
||||
}
|
||||
|
||||
JsonArray formatsJSON = object.getJsonArray("formats");
|
||||
if (formatsJSON != null)
|
||||
{
|
||||
List<Format> formats = new ArrayList<>();
|
||||
for (JsonObject formatJson : formatsJSON.getValuesAs(JsonObject.class))
|
||||
{
|
||||
Format format = Format.fromJSON(formatJson);
|
||||
if (format != null)
|
||||
formats.add(format);
|
||||
}
|
||||
track.setFormats(formats);
|
||||
}
|
||||
|
||||
ret.add(track);
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private File getDownloadDir() throws IOException
|
||||
|
@ -195,101 +318,216 @@ public class Downloader implements Consumer<DownloadTask>
|
|||
return downloadDir;
|
||||
}
|
||||
|
||||
private void download(DownloadTask task)
|
||||
private Track getTrackFromRequest(TrackRequest request, int idx)
|
||||
{
|
||||
Song song = task.request.getSong();
|
||||
chooseFormats(song);
|
||||
// if there's less tracks in the request than expected, fill the array
|
||||
while (idx >= request.getTracks().size())
|
||||
{
|
||||
Track track = new Track(request);
|
||||
track.setNumber(trackNumber);
|
||||
trackNumber++;
|
||||
request.addTrack(track);
|
||||
}
|
||||
return request.getTracks().get(idx);
|
||||
}
|
||||
|
||||
private void download(Promise<TrackRequest, Track> promise, boolean streamOutput) throws InterruptedException, ExecutionException
|
||||
{
|
||||
TrackRequest request = promise.getInput();
|
||||
Set<Format> uniqueFormats = new HashSet<>();
|
||||
|
||||
for (Track track : request.getTracks())
|
||||
{
|
||||
uniqueFormats.addAll(track.getFormats());
|
||||
}
|
||||
|
||||
List<Format> sortedFormats = sortFormats(uniqueFormats);
|
||||
String formatCodes = "";
|
||||
final List<Format> formats = song.getFormats();
|
||||
for (int i = 0; i < 3 && i < song.getFormats().size(); i++)
|
||||
final List<Format> formats = sortedFormats;
|
||||
for (int i = 0; i < 5 && i < sortedFormats.size(); i++)
|
||||
formatCodes += formats.get(i).getCode() + "/";
|
||||
|
||||
int downloadIdx = 0;
|
||||
|
||||
try
|
||||
{
|
||||
messageHandler.accept(task.request, null);
|
||||
String cmd = "/usr/bin/youtube-dl -x"
|
||||
+ " -f " + formatCodes + "worstaudio/bestaudio/worst/best"
|
||||
+ " --audio-format=wav"
|
||||
+ " --no-playlist"
|
||||
+ " -o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s "
|
||||
+ song.getUrl().toString();
|
||||
messageHandler.accept(request, null);
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(Chords.getSettings().getYtdlCommand());
|
||||
cmd.add("-x");
|
||||
cmd.add("-f=" + formatCodes + "worstaudio/bestaudio/worst/best");
|
||||
cmd.add("--audio-format=wav");
|
||||
cmd.add("--no-playlist");
|
||||
// cmd.add(" --extractor-args youtube:player_client=android";
|
||||
cmd.add("-N8");
|
||||
if (streamOutput)
|
||||
{
|
||||
cmd.add("--downloader=ffmpeg"); // download using FFMpeg
|
||||
cmd.add("--downloader-args=ffmpeg:-f wav -c:a pcm_s16le"); // tell FFMpeg to convert to wav
|
||||
cmd.add("-o"); // output to stdout
|
||||
cmd.add("-");
|
||||
} else
|
||||
{
|
||||
cmd.add("-o " + getDownloadDir().getAbsolutePath() + "/%(title)s.%(ext)s");
|
||||
}
|
||||
cmd.add(request.getUrl().toString());
|
||||
|
||||
Process exec = runCommand(cmd, 300);
|
||||
InputStream in = exec.getInputStream();
|
||||
String output = new String(in.readAllBytes(), Charset.defaultCharset());
|
||||
String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset());
|
||||
System.out.println(output);
|
||||
Matcher matcher = DESTINATION_PATTERN.matcher(output);
|
||||
if (matcher.find())
|
||||
song.setLocation(new File(matcher.group(1)));
|
||||
else if (exec.exitValue() != 0)
|
||||
throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error);
|
||||
// return true;
|
||||
Process exec = runCommand(cmd, streamOutput ? 0 : DOWNLOAD_TIMEOUT);
|
||||
|
||||
if (task.getDestination() != null)
|
||||
task.getDestination().accept(song);
|
||||
downloadQueue.remove(task);
|
||||
messageHandler.accept(task.request, null);
|
||||
if (streamOutput)
|
||||
{
|
||||
Scanner sc = new Scanner(exec.getErrorStream());
|
||||
while (sc.hasNextLine())
|
||||
{
|
||||
String line = sc.nextLine();
|
||||
System.out.println(line);
|
||||
Matcher streamMatcher = STREAM_PATTERN.matcher(line);
|
||||
if (streamMatcher.find())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exec.isAlive() && exec.exitValue() != 0)
|
||||
throw new RuntimeException("yt-dlp failed with error code " + exec.exitValue());
|
||||
|
||||
Track track = getTrackFromRequest(request, 0);
|
||||
BufferedInputStream inBuf = new BufferedInputStream(exec.getInputStream());
|
||||
inBuf.mark(128);
|
||||
final byte[] headerBytes = inBuf.readNBytes(80);
|
||||
String header = new String(headerBytes);
|
||||
System.out.println("streaming data header: " + header);
|
||||
if (!header.startsWith("RIFF"))
|
||||
throw new RuntimeException("Streaming data has bad header!");
|
||||
inBuf.reset();
|
||||
track.setInputStream(inBuf);
|
||||
promise.complete(track);
|
||||
track.setProgress(100.0);
|
||||
messageHandler.accept(request, null);
|
||||
} else
|
||||
{
|
||||
Scanner sc = new Scanner(exec.getInputStream());
|
||||
while (sc.hasNextLine())
|
||||
{
|
||||
String line = sc.nextLine();
|
||||
System.out.println(line);
|
||||
|
||||
Matcher itemMatcher = DOWNLOAD_ITEM_PATTERN.matcher(line);
|
||||
if (itemMatcher.find())
|
||||
{
|
||||
int idx = Integer.parseInt(itemMatcher.group(1)) - 1;
|
||||
downloadIdx = idx;
|
||||
}
|
||||
|
||||
Matcher progMatcher = PROGRESS_PATTERN.matcher(line);
|
||||
if (progMatcher.find())
|
||||
{
|
||||
getTrackFromRequest(request, downloadIdx).setProgress(Double.parseDouble(progMatcher.group(1)));
|
||||
messageHandler.accept(request, null);
|
||||
}
|
||||
|
||||
Matcher destMatcher = DESTINATION_PATTERN.matcher(line);
|
||||
if (destMatcher.find())
|
||||
{
|
||||
Track track = getTrackFromRequest(request, downloadIdx);
|
||||
|
||||
track.setLocation(new File(destMatcher.group(1)));
|
||||
|
||||
// this is currently our criteria for completion; submit the track and move on
|
||||
promise.complete(track);
|
||||
|
||||
track.setProgress(100.0);
|
||||
|
||||
messageHandler.accept(request, null);
|
||||
|
||||
downloadIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean exited = exec.waitFor(DOWNLOAD_TIMEOUT, TimeUnit.SECONDS);
|
||||
if (exited)
|
||||
{
|
||||
String error = new String(exec.getErrorStream().readAllBytes(), Charset.defaultCharset());
|
||||
|
||||
if (exec.exitValue() != 0)
|
||||
throw new RuntimeException("youtube-dl failed with error " + exec.exitValue() + ", output:\n" + error);
|
||||
} else
|
||||
{
|
||||
throw new RuntimeException("youtube-dl failed to exit.");
|
||||
}
|
||||
}
|
||||
|
||||
messageHandler.accept(request, null);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
Logger.getLogger(Downloader.class.getName()).log(Level.SEVERE, null, ex);
|
||||
if (messageHandler != null)
|
||||
messageHandler.accept(task.request, ex);
|
||||
downloadQueue.remove(task);
|
||||
messageHandler.accept(request, ex);
|
||||
//downloadQueue.remove(task);
|
||||
}
|
||||
}
|
||||
|
||||
private Process runCommand(String cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException
|
||||
private Process runCommand(List<String> cmd, int timeoutSecs) throws RuntimeException, IOException, InterruptedException
|
||||
{
|
||||
System.out.println("Running command: " + cmd);
|
||||
// Process exec = Runtime.getRuntime().exec().split(" "));
|
||||
Process exec = new ProcessBuilder(cmd.split(" ")).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
|
||||
boolean done = exec.waitFor(timeoutSecs, TimeUnit.SECONDS);
|
||||
if (!done)
|
||||
Process exec = new ProcessBuilder(cmd).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
|
||||
if (timeoutSecs > 0)
|
||||
{
|
||||
exec.destroyForcibly();
|
||||
throw new RuntimeException("Took too long, giving up.");
|
||||
scheduler.schedule(() ->
|
||||
{
|
||||
if (exec.isAlive())
|
||||
{
|
||||
exec.destroyForcibly();
|
||||
System.err.println("Process " + cmd + " took too long, killing process.");
|
||||
}
|
||||
}, timeoutSecs, TimeUnit.SECONDS);
|
||||
}
|
||||
return exec;
|
||||
}
|
||||
|
||||
public BiConsumer<SongRequest, Exception> getMessageHandler()
|
||||
public BiConsumer<TrackRequest, Exception> getMessageHandler()
|
||||
{
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
public void setMessageHandler(BiConsumer<SongRequest, Exception> messageHandler)
|
||||
public void setMessageHandler(BiConsumer<TrackRequest, Exception> messageHandler)
|
||||
{
|
||||
this.messageHandler = messageHandler;
|
||||
}
|
||||
|
||||
public List<DownloadTask> getDownloadQueue()
|
||||
public List<TrackRequest> getDownloadQueue()
|
||||
{
|
||||
return downloadQueue;
|
||||
return new ArrayList<>(inputQueue);
|
||||
}
|
||||
|
||||
public static class DownloadTask
|
||||
|
||||
// public static class DownloadTask
|
||||
// {
|
||||
//
|
||||
// private final TrackRequest request;
|
||||
// private final Consumer<Track> destination;
|
||||
//
|
||||
// public DownloadTask(TrackRequest request, Consumer<Track> destination)
|
||||
// {
|
||||
// this.request = request;
|
||||
// this.destination = destination;
|
||||
// }
|
||||
//
|
||||
// public TrackRequest getTrack()
|
||||
// {
|
||||
// return request;
|
||||
// }
|
||||
//
|
||||
// public Consumer<Track> getDestination()
|
||||
// {
|
||||
// return destination;
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
public List<TrackRequest> getDownloadingTracks()
|
||||
{
|
||||
|
||||
private final SongRequest request;
|
||||
private final Consumer<Song> destination;
|
||||
|
||||
public DownloadTask(SongRequest request, Consumer<Song> destination)
|
||||
{
|
||||
this.request = request;
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public SongRequest getSong()
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
public Consumer<Song> getDestination()
|
||||
{
|
||||
return destination;
|
||||
}
|
||||
|
||||
return downloadingTracks;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ package moe.nekojimi.chords;
|
|||
|
||||
import com.amihaiemil.eoyaml.Yaml;
|
||||
import com.amihaiemil.eoyaml.YamlMapping;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.Objects;
|
||||
import javax.json.JsonObject;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -17,13 +17,14 @@ import java.util.regex.Pattern;
|
|||
class Format
|
||||
{
|
||||
|
||||
private static final Pattern SIZE_PATTERN = Pattern.compile("\\b([0-9]+\\.?[0-9]*)([kkMmGg])i?[bB]\\b");
|
||||
private static final Pattern BITRATE_PATTERN = Pattern.compile("\\b([0-9]+)k(b(ps?))?\\b");
|
||||
|
||||
private final String code;
|
||||
private final String extension;
|
||||
private final String resolution;
|
||||
private final String note;
|
||||
private long size = -1;
|
||||
private int samplerate = -1;
|
||||
private boolean audioOnly = false;
|
||||
|
||||
public Format(String code, String extension, String resolution, String note)
|
||||
{
|
||||
|
@ -33,6 +34,28 @@ class Format
|
|||
this.note = note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read format info from a youtube-dl JSON response.
|
||||
*
|
||||
* @param object the JSON object from youtube-dl.
|
||||
* @return a new Format object if the JSON is valid, or null if not.
|
||||
*/
|
||||
public static Format fromJSON(JsonObject object)
|
||||
{
|
||||
String code = object.getString("format_id");
|
||||
String ext = object.getString("ext");
|
||||
// String res = object.getString()
|
||||
String res = object.getString("format");
|
||||
String note = "";
|
||||
Format ret = new Format(code, ext, res, note);
|
||||
int asr = object.getInt("asr", -1);
|
||||
int size = object.getInt("filesize", -1);
|
||||
ret.setSampleRate(asr);
|
||||
ret.setSize(size);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
public static Format fromYaml(YamlMapping yaml)
|
||||
{
|
||||
Format format = new Format(
|
||||
|
@ -55,46 +78,44 @@ class Format
|
|||
|
||||
public boolean isAudioOnly()
|
||||
{
|
||||
return resolution.trim().toLowerCase().contains("audio only");
|
||||
if (audioOnly)
|
||||
return true;
|
||||
return resolution.trim().toLowerCase().contains("audio only") || note.trim().toLowerCase().contains("tiny");
|
||||
}
|
||||
|
||||
public void setAudioOnly(boolean audioOnly)
|
||||
{
|
||||
this.audioOnly = audioOnly;
|
||||
}
|
||||
|
||||
public long getSize()
|
||||
{
|
||||
// try to find eg. "1.32MiB" inside 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;
|
||||
if (size != -1)
|
||||
return size;
|
||||
|
||||
}
|
||||
return -1;
|
||||
// try to find eg. "1.32MiB" inside note
|
||||
long size = Util.parseSize(note);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getBitrate()
|
||||
public int getSampleRate()
|
||||
{
|
||||
if (samplerate != -1)
|
||||
return samplerate;
|
||||
|
||||
// try to find eg. "51k" inside note
|
||||
Matcher matcher = BITRATE_PATTERN.matcher(note);
|
||||
if (matcher.find())
|
||||
{
|
||||
return Integer.parseInt(matcher.group(1)) * 1000;
|
||||
}
|
||||
return -1;
|
||||
return Util.parseSampleRate(note);
|
||||
}
|
||||
|
||||
public void setSampleRate(int samplerate)
|
||||
{
|
||||
this.samplerate = samplerate;
|
||||
}
|
||||
|
||||
public void setSize(long size)
|
||||
{
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
|
@ -123,4 +144,42 @@ class Format
|
|||
return "Format{" + "code=" + code + ", extension=" + extension + ", resolution=" + resolution + ", note=" + note + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int hash = 7;
|
||||
hash = 97 * hash + Objects.hashCode(this.extension);
|
||||
hash = 97 * hash + Objects.hashCode(this.resolution);
|
||||
hash = 97 * hash + (int) (this.size ^ (this.size >>> 32));
|
||||
hash = 97 * hash + this.samplerate;
|
||||
hash = 97 * hash + (this.audioOnly ? 1 : 0);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final Format other = (Format) obj;
|
||||
if (this.size != other.size)
|
||||
return false;
|
||||
if (this.samplerate != other.samplerate)
|
||||
return false;
|
||||
if (this.audioOnly != other.audioOnly)
|
||||
return false;
|
||||
if (!Objects.equals(this.code, other.code))
|
||||
return false;
|
||||
if (!Objects.equals(this.extension, other.extension))
|
||||
return false;
|
||||
if (!Objects.equals(this.resolution, other.resolution))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (C) 2022 jimj316
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class LocalBindSocketFactory extends SocketFactory
|
||||
{
|
||||
|
||||
private InetAddress localAddress;
|
||||
|
||||
public InetAddress getLocalAddress()
|
||||
{
|
||||
return localAddress;
|
||||
}
|
||||
|
||||
public void setLocalAddress(InetAddress localAddress)
|
||||
{
|
||||
this.localAddress = localAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException
|
||||
{
|
||||
return new LocalBoundSocket();
|
||||
// throw new RuntimeException("Trying to create an empty socket!");
|
||||
}
|
||||
|
||||
private InetSocketAddress findFreeSocketAddress()
|
||||
{
|
||||
return new InetSocketAddress(localAddress, findFreePort(localAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String remoteAddr, int remotePort) throws IOException, UnknownHostException
|
||||
{
|
||||
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException, UnknownHostException
|
||||
{
|
||||
return createSocket(InetAddress.getByName(remoteAddr), remotePort, localAddress, localPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress remoteAddr, int remotePort) throws IOException
|
||||
{
|
||||
return createSocket(remoteAddr, remotePort, localAddress, findFreePort(localAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress remoteAddr, int remotePort, InetAddress x, int localPort) throws IOException
|
||||
{
|
||||
System.out.println("Requested socket; " + localAddress + ":" + localPort + " -> " + remoteAddr + ":" + remotePort);
|
||||
Socket socket = new Socket(remoteAddr, remotePort, localAddress, localPort);
|
||||
System.out.println("Returned socket; " + socket.getLocalSocketAddress() + ":" + socket.getLocalPort() + " -> " + socket.getRemoteSocketAddress() + ":" + socket.getPort());
|
||||
return socket;
|
||||
}
|
||||
|
||||
private static int findFreePort(InetAddress localAddr)
|
||||
{
|
||||
int port = 0;
|
||||
// For ServerSocket port number 0 means that the port number is automatically allocated.
|
||||
try (ServerSocket socket = new ServerSocket(0, 0, localAddr))
|
||||
{
|
||||
// Disable timeout and reuse address after closing the socket.
|
||||
socket.setReuseAddress(true);
|
||||
port = socket.getLocalPort();
|
||||
} catch (IOException ignored)
|
||||
{
|
||||
}
|
||||
if (port > 0)
|
||||
{
|
||||
return port;
|
||||
}
|
||||
throw new RuntimeException("Could not find a free port");
|
||||
}
|
||||
|
||||
private class LocalBoundSocket extends Socket
|
||||
{
|
||||
|
||||
@Override
|
||||
public void bind(SocketAddress bindpoint) throws IOException
|
||||
{
|
||||
InetSocketAddress localAddress = findFreeSocketAddress();
|
||||
System.err.println("LocalBoundSocket NOT binding to " + bindpoint + ", using " + localAddress + " instead!");
|
||||
super.bind(localAddress);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,417 +0,0 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import com.amihaiemil.eoyaml.Yaml;
|
||||
import com.amihaiemil.eoyaml.YamlMapping;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.security.auth.login.LoginException;
|
||||
import moe.nekojimi.chords.commands.*;
|
||||
import moe.nekojimi.musicsearcher.providers.MetaSearcher;
|
||||
import moe.nekojimi.musicsearcher.providers.Searcher;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.entities.*;
|
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.managers.AudioManager;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import net.dv8tion.jda.api.utils.Compression;
|
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public final class Main extends ListenerAdapter
|
||||
{
|
||||
|
||||
private final File dataDirectory;
|
||||
private final File playlistsDirectory;
|
||||
|
||||
private MusicHandler musicHandler;
|
||||
private final Downloader downloader;
|
||||
private final Searcher searcher;
|
||||
private final QueueManager queueManager;
|
||||
private JDA jda;
|
||||
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
private final Command helpCommand;
|
||||
private PlayCommand playCommand;
|
||||
|
||||
private VoiceChannel currentVoiceChannel = null;
|
||||
|
||||
private int trackNumber = 1;
|
||||
|
||||
private final Map<String, Playlist> playlists = new HashMap<>();
|
||||
|
||||
/**
|
||||
* @param args the command line arguments
|
||||
*/
|
||||
public static void main(String[] args) throws LoginException, IOException
|
||||
{
|
||||
// We only need 2 gateway intents enabled for this example:
|
||||
EnumSet<GatewayIntent> intents = EnumSet.of(
|
||||
// We need messages in guilds to accept commands from users
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
// We need voice states to connect to the voice channel
|
||||
GatewayIntent.GUILD_VOICE_STATES,
|
||||
GatewayIntent.GUILD_MEMBERS
|
||||
);
|
||||
|
||||
JDABuilder builder = JDABuilder.createDefault(args[0], intents);
|
||||
|
||||
// Disable parts of the cache
|
||||
builder.disableCache(CacheFlag.MEMBER_OVERRIDES);
|
||||
// Enable the bulk delete event
|
||||
builder.setBulkDeleteSplittingEnabled(false);
|
||||
// Disable compression (not recommended)
|
||||
builder.setCompression(Compression.NONE);
|
||||
// Set activity (like "playing Something")
|
||||
builder.setActivity(Activity.playing("music!"));
|
||||
|
||||
final Main listener = new Main();
|
||||
|
||||
builder.addEventListeners(listener);
|
||||
builder.setAutoReconnect(true);
|
||||
|
||||
JDA jda = builder.build();
|
||||
listener.setJda(jda);
|
||||
}
|
||||
private final Consumer<Song> nowPlayingConsumer = (Song song) ->
|
||||
{
|
||||
if (song != null)
|
||||
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, song.toString()));
|
||||
else
|
||||
jda.getPresence().setActivity(null);
|
||||
};
|
||||
|
||||
private final BiConsumer<SongRequest, Exception> downloaderMessageHandler = (SongRequest request, Exception ex) ->
|
||||
{
|
||||
Song song = request.getSong();
|
||||
// TextChannel channel = jda.getTextChannelById(song.getRequestedIn());
|
||||
// String bracketNo = "[" + song.getNumber() + "] ";
|
||||
if (ex == null)
|
||||
if (song.getLocation() != null)
|
||||
{
|
||||
request.respond("Finished downloading " + song + ", added to queue!");
|
||||
log("DOWN", "Downloaded " + song);
|
||||
} else
|
||||
{
|
||||
Format format = song.getBestFormat();
|
||||
String formatDetails = "";
|
||||
if (format != null)
|
||||
{
|
||||
final int bitrate = format.getBitrate() / 1000;
|
||||
final long size = format.getSize();
|
||||
String sizeFmt = (size <= 0 ? "?.??" : String.format("%.2f", size / (1024.0 * 1024.0))) + "MiB";
|
||||
String bitFmt = (bitrate <= 0 ? "??" : bitrate) + "k";
|
||||
formatDetails = " (" + bitFmt + ", " + sizeFmt + ")";
|
||||
}
|
||||
request.respond("Now downloading " + song + formatDetails + " ...");
|
||||
log("DOWN", "Downloading " + song + "...");
|
||||
}
|
||||
else
|
||||
{
|
||||
request.respond("Failed to download " + song + "! Reason: " + ex.getMessage());
|
||||
log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public Main() throws IOException
|
||||
{
|
||||
log("INFO", "Starting up...");
|
||||
|
||||
// init dirs
|
||||
dataDirectory = new File(System.getProperty("user.dir"));
|
||||
playlistsDirectory = initDirectory(dataDirectory, "playlists");
|
||||
|
||||
// init downloader
|
||||
downloader = new Downloader();
|
||||
downloader.setMessageHandler(downloaderMessageHandler);
|
||||
|
||||
// init searcher
|
||||
searcher = MetaSearcher.loadYAML(new File("searchproviders.yml"));
|
||||
|
||||
// init queue manager
|
||||
queueManager = new QueueManager();
|
||||
|
||||
// init commands
|
||||
addCommand(new JoinCommand(this));
|
||||
addCommand(new LeaveCommand(this));
|
||||
playCommand = new PlayCommand(this);
|
||||
addCommand(playCommand);
|
||||
addCommand(new QueueCommand(this));
|
||||
addCommand(new RemoveCommand(this));
|
||||
addCommand(new RestartCommand(this));
|
||||
addCommand(new SkipCommand(this));
|
||||
helpCommand = new HelpCommand(this);
|
||||
addCommand(helpCommand);
|
||||
|
||||
// load playlists
|
||||
loadPlaylists();
|
||||
|
||||
log("INFO", "Started OK!");
|
||||
}
|
||||
|
||||
|
||||
private void addCommand(Command command)
|
||||
{
|
||||
commands.put(command.getKeyword(), command);
|
||||
}
|
||||
|
||||
public void setJda(JDA jda)
|
||||
{
|
||||
this.jda = jda;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildVoiceLeave(GuildVoiceLeaveEvent event)
|
||||
{
|
||||
if (this.currentVoiceChannel == null)
|
||||
return;
|
||||
|
||||
final VoiceChannel channelLeft = event.getChannelLeft();
|
||||
|
||||
if (channelLeft.getMembers().isEmpty())
|
||||
if (channelLeft == currentVoiceChannel)
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildMessageReceived(GuildMessageReceivedEvent event)
|
||||
{
|
||||
Message message = event.getMessage();
|
||||
User author = message.getAuthor();
|
||||
String content = message.getContentRaw();
|
||||
Guild guild = event.getGuild();
|
||||
|
||||
// Ignore message if it's not in a music channel
|
||||
if (!event.getChannel().getName().contains("music"))
|
||||
return;
|
||||
|
||||
// Ignore message if bot
|
||||
if (author.isBot())
|
||||
return;
|
||||
|
||||
log("MESG", "G:" + guild.getName() + " A:" + author.getName() + " C:" + content);
|
||||
|
||||
try
|
||||
{
|
||||
URL parseURL = new URL(content.trim());
|
||||
playCommand.call(event, List.of(parseURL.toExternalForm()));
|
||||
} catch (MalformedURLException ex)
|
||||
{
|
||||
// not a URL, then
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
String[] split = content.split("\\s+", 2);
|
||||
String cmd = split[0].toLowerCase();
|
||||
|
||||
if (!cmd.startsWith("!"))
|
||||
return; // doesn't start with prefix char
|
||||
|
||||
cmd = cmd.substring(1); // strip prefix char
|
||||
|
||||
String arg = "";
|
||||
if (split.length > 1)
|
||||
arg = split[1];
|
||||
|
||||
if (commands.containsKey(cmd))
|
||||
{
|
||||
Command command = commands.get(cmd);
|
||||
command.call(event, List.of(arg));
|
||||
} else
|
||||
{
|
||||
helpCommand.call(event, List.of(arg));
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
event.getChannel().sendMessage("Error in command! " + ex.getMessage()).queue();
|
||||
log("UERR", "Command error:" + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Song queueDownload(SongRequest request)
|
||||
{
|
||||
Song song;
|
||||
if (request.getUrl() != null)
|
||||
{
|
||||
song = new Song(request.getUrl());
|
||||
} else
|
||||
{
|
||||
// interpret search result
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
if (request.getRequestMessage() != null)
|
||||
{
|
||||
song.setRequestedBy(request.getRequestMessage().getAuthor().getName());
|
||||
song.setRequestedIn(request.getRequestMessage().getChannel().getId());
|
||||
}
|
||||
song.setNumber(trackNumber);
|
||||
trackNumber++;
|
||||
request.setSong(song);
|
||||
downloader.accept(new Downloader.DownloadTask(request, queueManager));
|
||||
request.respond("Request pending...");
|
||||
return song;
|
||||
}
|
||||
|
||||
// public Song queueDownload(final URL url, GuildMessageReceivedEvent event)
|
||||
// {
|
||||
// Song song = new Song(url);
|
||||
// song.setRequestedBy(event.getAuthor().getName());
|
||||
// song.setRequestedIn(event.getChannel().getId());
|
||||
// song.setNumber(trackNumber);
|
||||
// trackNumber++;
|
||||
// downloader.accept(new Downloader.DownloadTask(song, musicHandler));
|
||||
// return song;
|
||||
// }
|
||||
//
|
||||
// public Song queueDownload(Result res, GuildMessageReceivedEvent event)
|
||||
// {
|
||||
// Song song = queueDownload(res.getLink(), event);
|
||||
// song.setArtist(res.getArtist());
|
||||
// song.setTitle(res.getTitle());
|
||||
// song.setNumber(trackNumber);
|
||||
// return song;
|
||||
// }
|
||||
public void setStatus(Song nowPlaying)
|
||||
{
|
||||
jda.getPresence().setActivity(Activity.listening(nowPlaying.toString()));
|
||||
}
|
||||
|
||||
public void clearStatus()
|
||||
{
|
||||
jda.getPresence().setActivity(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to requested channel and start echo handler
|
||||
*
|
||||
* @param channel
|
||||
* The channel to connect to
|
||||
*/
|
||||
public void connectTo(VoiceChannel channel)
|
||||
{
|
||||
Guild guild = channel.getGuild();
|
||||
// Get an audio manager for this guild, this will be created upon first use for each guild
|
||||
AudioManager audioManager = guild.getAudioManager();
|
||||
musicHandler = new MusicHandler();
|
||||
queueManager.setHandler(musicHandler);
|
||||
// Create our Send/Receive handler for the audio connection
|
||||
// EchoHandler handler = new EchoHandler();
|
||||
|
||||
// The order of the following instructions does not matter!
|
||||
// Set the sending handler to our echo system
|
||||
audioManager.setSendingHandler(musicHandler);
|
||||
// Set the receiving handler to the same echo system, otherwise we can't echo anything
|
||||
// audioManager.setReceivingHandler(handler);
|
||||
// Connect to the voice channel
|
||||
audioManager.openAudioConnection(channel);
|
||||
currentVoiceChannel = channel;
|
||||
|
||||
musicHandler.setNowPlayingConsumer(nowPlayingConsumer);
|
||||
}
|
||||
|
||||
public void disconnect()
|
||||
{
|
||||
if (currentVoiceChannel != null)
|
||||
{
|
||||
Guild guild = currentVoiceChannel.getGuild();
|
||||
AudioManager audioManager = guild.getAudioManager();
|
||||
audioManager.setSendingHandler(null);
|
||||
audioManager.closeAudioConnection();
|
||||
musicHandler = null;
|
||||
currentVoiceChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void log(String type, String message)
|
||||
{
|
||||
System.out.println(type + " " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + message);
|
||||
}
|
||||
|
||||
public File initDirectory(File parent, String name) throws IOException
|
||||
{
|
||||
File ret = new File(parent, name);
|
||||
if (!ret.exists())
|
||||
Files.createDirectories(ret.toPath());
|
||||
if (!ret.canRead())
|
||||
throw new RuntimeException("Cannot read directory " + ret.getAbsolutePath() + "!");
|
||||
if (!ret.canWrite())
|
||||
throw new RuntimeException("Cannot write to directory " + ret.getAbsolutePath() + "!");
|
||||
return ret;
|
||||
}
|
||||
|
||||
private void loadPlaylists()
|
||||
{
|
||||
File[] files = playlistsDirectory.listFiles((File file, String name) -> name.endsWith(".yaml"));
|
||||
for (File file : files)
|
||||
{
|
||||
try
|
||||
{
|
||||
YamlMapping map = Yaml.createYamlInput(file).readYamlMapping();
|
||||
Playlist playlist = Playlist.fromYaml(map);
|
||||
playlists.put(playlist.getName(), playlist);
|
||||
} catch (IOException ex)
|
||||
{
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MusicHandler getMusicHandler()
|
||||
{
|
||||
return musicHandler;
|
||||
}
|
||||
|
||||
public Downloader getDownloader()
|
||||
{
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public Searcher getSearcher()
|
||||
{
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public JDA getJda()
|
||||
{
|
||||
return jda;
|
||||
}
|
||||
|
||||
public VoiceChannel getCurrentVoiceChannel()
|
||||
{
|
||||
return currentVoiceChannel;
|
||||
}
|
||||
public QueueManager getQueueManager()
|
||||
{
|
||||
return queueManager;
|
||||
}
|
||||
|
||||
public int getTrackNumber()
|
||||
{
|
||||
return trackNumber;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -7,54 +7,50 @@ package moe.nekojimi.chords;
|
|||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.sound.sampled.*;
|
||||
import net.dv8tion.jda.api.audio.AudioSendHandler;
|
||||
import org.apache.commons.io.input.buffer.CircularByteBuffer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class MusicHandler implements AudioSendHandler, Closeable
|
||||
public class MusicHandler implements AudioSendHandler, Closeable, Consumer<Track>
|
||||
{
|
||||
|
||||
// private QueueThing<?, Track> queueManager;
|
||||
private QueueManager queueManager;
|
||||
// private final LinkedList<Song> songQueue = new LinkedList<>();
|
||||
// private final LinkedList<Track> trackQueue = new LinkedList<>();
|
||||
// private final Queue<byte[]> queue = new ConcurrentLinkedQueue<>();
|
||||
private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
|
||||
// private final CircularByteBuffer audioBuffer = new CircularByteBuffer(3840 * 1024);
|
||||
private boolean shouldPlay = true;
|
||||
private int byteCount;
|
||||
|
||||
private boolean arrayErr = false;
|
||||
private Consumer<Song> nowPlayingConsumer;
|
||||
private Consumer<Track> nowPlayingConsumer;
|
||||
|
||||
public void setNowPlayingConsumer(Consumer<Song> nowPlayingConsumer)
|
||||
public void setNowPlayingConsumer(Consumer<Track> nowPlayingConsumer)
|
||||
{
|
||||
this.nowPlayingConsumer = nowPlayingConsumer;
|
||||
}
|
||||
|
||||
private Song currentSong;
|
||||
private TrackPlayer player;
|
||||
|
||||
private File debugOutFile;
|
||||
private BufferedOutputStream debugOut;
|
||||
private Track currentTrack;
|
||||
// private TrackPlayer player;
|
||||
private final List<TrackPlayer> playingTracks = new ArrayList<>();
|
||||
|
||||
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 accept(Track t)
|
||||
{
|
||||
play(t);
|
||||
}
|
||||
|
||||
void setQueueManager(QueueManager manager)
|
||||
|
@ -62,66 +58,74 @@ public class MusicHandler implements AudioSendHandler, Closeable
|
|||
queueManager = manager;
|
||||
}
|
||||
|
||||
public void playNext()
|
||||
|
||||
public void playOver(Track track)
|
||||
{
|
||||
nextSong(true);
|
||||
|
||||
}
|
||||
|
||||
// public boolean restartSong()
|
||||
// {
|
||||
//// songQueue.addFirst(currentSong);
|
||||
// currentSong = null;
|
||||
// return nextSong(true);
|
||||
// }
|
||||
|
||||
private boolean nextSong()
|
||||
public boolean play(Track track)
|
||||
{
|
||||
return nextSong(false);
|
||||
return play(track, false);
|
||||
}
|
||||
|
||||
public boolean nextSong(boolean immediate)
|
||||
public boolean play(Track track, boolean immediate)
|
||||
{
|
||||
if (track == currentTrack)
|
||||
return false;
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
System.out.println("Immediate next - clearing buffer");
|
||||
audioBuffer.clear();
|
||||
playingTracks.clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (currentSong != null)
|
||||
if (currentTrack != null)
|
||||
{
|
||||
if (!currentSong.isKept())
|
||||
currentSong.delete();
|
||||
currentSong = null;
|
||||
if (!currentTrack.isKept())
|
||||
currentTrack.delete();
|
||||
currentTrack = null;
|
||||
}
|
||||
currentSong = queueManager.nextSongNeeded();
|
||||
currentTrack = track;
|
||||
if (nowPlayingConsumer != null)
|
||||
nowPlayingConsumer.accept(currentSong);
|
||||
if (currentSong == null)
|
||||
nowPlayingConsumer.accept(currentTrack);
|
||||
if (currentTrack == null)
|
||||
{
|
||||
System.out.println("End of queue.");
|
||||
debugOut.flush();
|
||||
return false;
|
||||
}
|
||||
System.out.println("Playing song " + currentSong.getLocation().getAbsolutePath());
|
||||
// System.out.println("Playing track " + currentTrack.getLocation().getAbsolutePath());
|
||||
arrayErr = false;
|
||||
byteCount = 3840;
|
||||
player = new TrackPlayer(currentSong);
|
||||
TrackPlayer player = new TrackPlayer(currentTrack);
|
||||
playingTracks.add(player);
|
||||
// System.out.println("Queue filled to " + audioBuffer.getCurrentNumberOfBytes());
|
||||
return true;
|
||||
} catch (UnsupportedAudioFileException | IOException ex)
|
||||
{
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
Logger.getLogger(Chords.class.getName()).log(Level.SEVERE, null, ex);
|
||||
currentTrack = null;
|
||||
requestTrack();
|
||||
} finally
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean requestTrack()
|
||||
{
|
||||
if (isPlaying())
|
||||
System.out.println("How did we get here?");
|
||||
System.out.println("MusicHandler requesting track...");
|
||||
List<QueueThing.Promise<Track, Track>> request = queueManager.request(1, this);
|
||||
// Queuemanager will syncronously attempt to call play()
|
||||
return !playingTracks.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isPlaying()
|
||||
{
|
||||
return player != null;
|
||||
return !playingTracks.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isShouldPlay()
|
||||
|
@ -132,14 +136,19 @@ public class MusicHandler implements AudioSendHandler, Closeable
|
|||
public void setShouldPlay(boolean shouldPlay)
|
||||
{
|
||||
if (!this.shouldPlay && shouldPlay)
|
||||
nextSong();
|
||||
requestTrack();
|
||||
this.shouldPlay = shouldPlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProvide()
|
||||
{
|
||||
return player != null && player.has(1);
|
||||
if (playingTracks.isEmpty())
|
||||
return false;
|
||||
for (TrackPlayer player : playingTracks)
|
||||
if (player.has(1))
|
||||
return true;
|
||||
return false;
|
||||
// If we have something in our buffer we can provide it to the send system
|
||||
// return audioBuffer.getCurrentNumberOfBytes() > byteCount && shouldPlay;
|
||||
}
|
||||
|
@ -148,28 +157,36 @@ public class MusicHandler implements AudioSendHandler, Closeable
|
|||
public ByteBuffer provide20MsAudio()
|
||||
{
|
||||
ByteBuffer ret = ByteBuffer.allocate(byteCount);
|
||||
while (ret.position() < byteCount && player != null)
|
||||
while (ret.position() < byteCount && !playingTracks.isEmpty())
|
||||
{
|
||||
// System.out.println("Position: " + ret.position() + " Remaining: " + ret.remaining());
|
||||
try
|
||||
boolean outOfInput = true;
|
||||
List<ByteBuffer> mixes = new ArrayList<>();
|
||||
List<TrackPlayer> emptyPlayers = new ArrayList<>();
|
||||
for (TrackPlayer player : playingTracks)
|
||||
{
|
||||
ByteBuffer read = player.read(ret.remaining());
|
||||
// System.out.println("SAMPLES from player: " + Util.printSamples(read));
|
||||
// System.out.println("Wanted: " + byteCount + " Space:" + space + " Available: " + din.available() + " To read: " + bytesToRead + " Read: " + read);
|
||||
|
||||
// System.out.println("Read: " + read.remaining());
|
||||
ret.put(read);
|
||||
} catch (TrackPlayer.OutOfInputException | IOException ex)
|
||||
{
|
||||
// System.out.println("Track ended, starting next.");
|
||||
boolean foundNext = nextSong();
|
||||
|
||||
if (!foundNext)
|
||||
try
|
||||
{
|
||||
// System.out.println("Out of tracks!");
|
||||
break;
|
||||
ByteBuffer read = player.read(ret.remaining());
|
||||
if (ret.limit() + read.position() >= byteCount)
|
||||
outOfInput = false;
|
||||
mixes.add(read);
|
||||
// ret.put(read);
|
||||
} catch (TrackPlayer.OutOfInputException | IOException ex)
|
||||
{
|
||||
// System.out.println("Track player " + player + " stopped giving input: " + ex.getMessage());
|
||||
emptyPlayers.add(player);
|
||||
// System.out.println("Track ended, starting next.");
|
||||
// outOfInput = true;
|
||||
}
|
||||
}
|
||||
playingTracks.removeAll(emptyPlayers);
|
||||
ret.put(mixBuffers(mixes));
|
||||
if (outOfInput)
|
||||
{
|
||||
boolean foundNext = requestTrack();
|
||||
if (!foundNext)
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
// System.out.println("Buffer filled, submitting.");
|
||||
|
@ -179,59 +196,9 @@ public class MusicHandler implements AudioSendHandler, Closeable
|
|||
|
||||
}
|
||||
|
||||
// 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 Song getCurrentSong()
|
||||
public Track getCurrentTrack()
|
||||
{
|
||||
return currentSong;
|
||||
return currentTrack;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -245,4 +212,48 @@ public class MusicHandler implements AudioSendHandler, Closeable
|
|||
{
|
||||
}
|
||||
|
||||
private ByteBuffer mixBuffers(List<ByteBuffer> mixes)
|
||||
{
|
||||
// System.out.println("Mixing " + mixes.size() + " buffers");
|
||||
if (mixes.size() == 1)
|
||||
return mixes.get(0);
|
||||
|
||||
int maxSize = 0;
|
||||
for (ByteBuffer buf : mixes)
|
||||
{
|
||||
if (buf.limit() > maxSize)
|
||||
maxSize = buf.position();
|
||||
}
|
||||
ByteBuffer ret = ByteBuffer.allocate(maxSize);
|
||||
|
||||
for (int i = 0; i < ret.limit(); i++)
|
||||
{
|
||||
int byteTotal = 0;
|
||||
int mixCount = 0;
|
||||
for (ByteBuffer buf : mixes)
|
||||
{
|
||||
if (i < buf.limit())
|
||||
{
|
||||
byteTotal += buf.get(i);
|
||||
mixCount++;
|
||||
}
|
||||
}
|
||||
double avg = ((double) byteTotal) / mixCount;
|
||||
byte byteVal = (byte) Math.round(avg);
|
||||
ret.put(byteVal);
|
||||
}
|
||||
ret.rewind();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public boolean skipTrack()
|
||||
{
|
||||
if (!isPlaying())
|
||||
return false;
|
||||
playingTracks.clear();
|
||||
currentTrack = null;
|
||||
requestTrack();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,14 +30,14 @@ import java.util.logging.Logger;
|
|||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class Playlist implements Consumer<Song>
|
||||
public class Playlist implements Consumer<Track>
|
||||
{
|
||||
|
||||
private static final int SHUFFLE_DONT_REPEAT_LAST = 3;
|
||||
|
||||
private final String name;
|
||||
private final List<Song> songs = new ArrayList<>();
|
||||
private final LinkedList<Song> playHistory = new LinkedList<>();
|
||||
private final List<Track> tracks = new ArrayList<>();
|
||||
private final LinkedList<Track> playHistory = new LinkedList<>();
|
||||
|
||||
public Playlist(String name)
|
||||
{
|
||||
|
@ -46,25 +46,25 @@ public class Playlist implements Consumer<Song>
|
|||
|
||||
public YamlMapping toYaml()
|
||||
{
|
||||
YamlSequenceBuilder songList = Yaml.createYamlSequenceBuilder();
|
||||
for (Song song : songs)
|
||||
songList = songList.add(song.toYaml());
|
||||
YamlSequenceBuilder trackList = Yaml.createYamlSequenceBuilder();
|
||||
for (Track track : tracks)
|
||||
trackList = trackList.add(track.toYaml());
|
||||
|
||||
return Yaml.createYamlMappingBuilder()
|
||||
.add("name", name)
|
||||
.add("songs", songList.build())
|
||||
.add("tracks", trackList.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Playlist fromYaml(YamlMapping yaml)
|
||||
{
|
||||
Playlist ret = new Playlist(yaml.string("name"));
|
||||
YamlSequence songList = yaml.value("songs").asSequence();
|
||||
for (int i = 0; i < songList.size(); i++)
|
||||
YamlSequence trackList = yaml.value("tracks").asSequence();
|
||||
for (int i = 0; i < trackList.size(); i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
ret.addSong(Song.fromYaml(songList.yamlMapping(i)));
|
||||
ret.addTrack(Track.fromYaml(trackList.yamlMapping(i)));
|
||||
} catch (MalformedURLException ex)
|
||||
{
|
||||
Logger.getLogger(Playlist.class.getName()).log(Level.SEVERE, null, ex);
|
||||
|
@ -73,10 +73,10 @@ public class Playlist implements Consumer<Song>
|
|||
return ret;
|
||||
}
|
||||
|
||||
public void addSong(Song song)
|
||||
public void addTrack(Track track)
|
||||
{
|
||||
song.setKept(true);
|
||||
songs.add(song);
|
||||
track.setKept(true);
|
||||
tracks.add(track);
|
||||
}
|
||||
|
||||
public String getName()
|
||||
|
@ -84,17 +84,17 @@ public class Playlist implements Consumer<Song>
|
|||
return name;
|
||||
}
|
||||
|
||||
public List<Song> getSongs()
|
||||
public List<Track> getTracks()
|
||||
{
|
||||
return songs;
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public Song getNextSong()
|
||||
public Track getNextTrack()
|
||||
{
|
||||
Song ret;
|
||||
Track ret;
|
||||
|
||||
// copy the song list
|
||||
List<Song> toShuffle = new LinkedList<>(songs);
|
||||
// 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--)
|
||||
|
@ -114,9 +114,9 @@ public class Playlist implements Consumer<Song>
|
|||
}
|
||||
|
||||
@Override
|
||||
public void accept(Song t)
|
||||
public void accept(Track t)
|
||||
{
|
||||
addSong(t);
|
||||
addTrack(t);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
@ -24,75 +25,122 @@ import java.util.function.Consumer;
|
|||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class QueueManager implements Consumer<Song>
|
||||
public class QueueManager extends QueueThing<Track, Track>
|
||||
{
|
||||
private final Queue<Song> jukeboxQueue;
|
||||
|
||||
private final int QUEUE_TARGET_SIZE = 5;
|
||||
|
||||
private Track restartingTrack = null;
|
||||
// private final PriorityQueue<Track> jukeboxQueue = new PriorityQueue<>();
|
||||
private QueueThing<?, Track> trackSource;
|
||||
private Playlist playlist;
|
||||
private MusicHandler handler;
|
||||
|
||||
public QueueManager()
|
||||
{
|
||||
jukeboxQueue = new LinkedList<>();
|
||||
super(new PriorityQueue<Track>());
|
||||
queueTargetSize = QUEUE_TARGET_SIZE;
|
||||
// jukeboxQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Song t)
|
||||
protected void notifyNewInput()
|
||||
{
|
||||
jukeboxQueue.add(t);
|
||||
super.notifyNewInput();
|
||||
|
||||
if (!handler.isPlaying())
|
||||
handler.playNext();
|
||||
// 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 song has ended, or if
|
||||
* 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 Song nextSongNeeded()
|
||||
{
|
||||
// if there's anything in the queue, play that first
|
||||
if (!jukeboxQueue.isEmpty())
|
||||
{
|
||||
return jukeboxQueue.poll();
|
||||
}
|
||||
// otherwise if there's a playlist, shuffle from that
|
||||
else if (playlist != null)
|
||||
{
|
||||
return playlist.getNextSong();
|
||||
}
|
||||
// otherwise stop playing
|
||||
else
|
||||
return null;
|
||||
}
|
||||
// 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 addSong(Song song)
|
||||
public void addTrack(Track track)
|
||||
{
|
||||
System.out.println("Song added to queue: " + song.getLocation().getAbsolutePath());
|
||||
jukeboxQueue.add(song);
|
||||
System.out.println("Track added to queue: " + track.getLocation().getAbsolutePath());
|
||||
inputQueue.add(track);
|
||||
|
||||
}
|
||||
|
||||
public boolean removeSong(int i)
|
||||
public boolean removeTrack(int i)
|
||||
{
|
||||
try
|
||||
{
|
||||
return jukeboxQueue.remove((Song) jukeboxQueue.toArray()[i]);
|
||||
return inputQueue.remove((Track) inputQueue.toArray()[i]);
|
||||
} catch (ArrayIndexOutOfBoundsException ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeSong(Song song)
|
||||
public boolean removeTrack(Track track)
|
||||
{
|
||||
return jukeboxQueue.remove(song);
|
||||
return inputQueue.remove(track);
|
||||
}
|
||||
|
||||
public void setHandler(MusicHandler handler)
|
||||
|
@ -101,9 +149,9 @@ public class QueueManager implements Consumer<Song>
|
|||
handler.setQueueManager(this);
|
||||
}
|
||||
|
||||
public Queue<Song> getJukeboxQueue()
|
||||
public Queue<Track> getJukeboxQueue()
|
||||
{
|
||||
return jukeboxQueue;
|
||||
return inputQueue;
|
||||
}
|
||||
|
||||
public Playlist getPlaylist()
|
||||
|
@ -116,8 +164,24 @@ public class QueueManager implements Consumer<Song>
|
|||
this.playlist = playlist;
|
||||
}
|
||||
|
||||
public boolean restartSong()
|
||||
public boolean restartTrack()
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright (C) 2023 jimj316
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public abstract class QueueThing<I, O> implements Consumer<I>
|
||||
{
|
||||
// TODO: better name
|
||||
protected final Queue<I> inputQueue;
|
||||
protected final List<QueueThing<?, I>> sources = new ArrayList<>();
|
||||
protected final List<QueueThing<O, ?>> sinks = new ArrayList<>();
|
||||
protected final Queue<Promise<I, O>> pendingPromises = new LinkedList<>();
|
||||
|
||||
protected int queueTargetSize = 0;
|
||||
|
||||
protected QueueThing(Queue<I> inputQueue)
|
||||
{
|
||||
this.inputQueue = inputQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(I t)
|
||||
{
|
||||
// FIXME: this causes queue-jumping
|
||||
// if we've got pending promises, fullfill them, otherwise just put it in the queue and notify the sinks
|
||||
if (pendingPromises.isEmpty())
|
||||
{
|
||||
inputQueue.add(t);
|
||||
notifyNewInput();
|
||||
} else
|
||||
{
|
||||
Promise<I, O> promise = pendingPromises.poll();
|
||||
System.out.println(this.getClass().getSimpleName() + " now has " + pendingPromises + " promises.");
|
||||
promise.setInput(t);
|
||||
handlePromise(promise);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to notify downstream modules that there's input upstream -
|
||||
* consider requesting it to pull it through
|
||||
*/
|
||||
protected void notifyNewInput()
|
||||
{
|
||||
if (inputQueue.size() < queueTargetSize)
|
||||
demandInput(queueTargetSize - inputQueue.size());
|
||||
|
||||
for (QueueThing<O, ?> sink : sinks)
|
||||
{
|
||||
sink.notifyNewInput();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Promise<I, O>> request(int count, Consumer<O> destination)
|
||||
{
|
||||
List<Promise<I, O>> ret = new ArrayList<>();
|
||||
int demandsForClient = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
I input = null;
|
||||
if (!inputQueue.isEmpty())
|
||||
{
|
||||
input = inputQueue.poll();
|
||||
}
|
||||
|
||||
Promise<I, O> promise = new Promise<>(input, destination);
|
||||
|
||||
boolean ok = handlePromise(promise);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
// we got a promise of output so we can tell the client
|
||||
ret.add(promise);
|
||||
} else
|
||||
{
|
||||
// we need to get more input from sources
|
||||
demandsForClient++;
|
||||
}
|
||||
}
|
||||
|
||||
int demandsForMe = queueTargetSize - inputQueue.size();
|
||||
int demandsTotal = demandsForMe + demandsForClient;
|
||||
|
||||
if (demandsTotal > 0)
|
||||
{
|
||||
// try to get more input from sources
|
||||
int sourcePromises = demandInput(demandsForClient);
|
||||
// each promise of input we get represents a promise of output we can give
|
||||
for (int i = 0; i < sourcePromises && i < demandsForClient; i++)
|
||||
{
|
||||
Promise<I, O> myPromise = new Promise<>(destination);
|
||||
pendingPromises.add(myPromise);
|
||||
System.out.println(this.getClass().getSimpleName() + " has made " + pendingPromises + " promises.");
|
||||
ret.add(myPromise);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private boolean handlePromise(Promise<I, O> promise)
|
||||
{
|
||||
boolean ok;
|
||||
ok = completePromise(promise);
|
||||
return ok;
|
||||
}
|
||||
|
||||
protected abstract boolean completePromise(Promise<I, O> request);
|
||||
|
||||
/**
|
||||
* Requests from sources a certain about of input, to be provided later.
|
||||
*
|
||||
* @param count the number of input items to request.
|
||||
* @return a number Promises of input items. May be more or less than count.
|
||||
*/
|
||||
protected int demandInput(int count)
|
||||
{
|
||||
int ret = 0;
|
||||
for (QueueThing<?, I> source : sources)
|
||||
{
|
||||
List<Promise<?, I>> promises = (List<Promise<?, I>>) source.request(count - ret, this);
|
||||
ret += promises.size();
|
||||
if (ret >= count)
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void addSource(QueueThing<?, I> source)
|
||||
{
|
||||
sources.add(source);
|
||||
source.addSink(this);
|
||||
}
|
||||
|
||||
public void removeSource(QueueThing<?, I> source)
|
||||
{
|
||||
sources.remove(source);
|
||||
source.removeSink(this);
|
||||
}
|
||||
|
||||
private void addSink(QueueThing<O, ?> sink)
|
||||
{
|
||||
sinks.add(sink);
|
||||
}
|
||||
|
||||
private void removeSink(QueueThing<O, ?> sink)
|
||||
{
|
||||
sinks.remove(sink);
|
||||
}
|
||||
|
||||
public static class Promise<I, O>
|
||||
{
|
||||
|
||||
private I input;
|
||||
private final Consumer<O> output;
|
||||
|
||||
public Promise(I input, Consumer<O> output)
|
||||
{
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
public Promise(Consumer<O> output)
|
||||
{
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
public void setInput(I input)
|
||||
{
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
public void complete(O out)
|
||||
{
|
||||
output.accept(out);
|
||||
}
|
||||
|
||||
public I getInput()
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
// protected void submit(O output)
|
||||
// {
|
||||
// if (nextStage != null)
|
||||
// nextStage.accept(output);
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2023 jimj316
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import com.amihaiemil.eoyaml.Yaml;
|
||||
import com.amihaiemil.eoyaml.YamlInput;
|
||||
import com.amihaiemil.eoyaml.YamlMapping;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class Settings
|
||||
{
|
||||
|
||||
private final YamlMapping mapping;
|
||||
|
||||
public Settings(File file) throws FileNotFoundException, IOException
|
||||
{
|
||||
YamlInput input = Yaml.createYamlInput(file);
|
||||
mapping = input.readYamlMapping();
|
||||
|
||||
}
|
||||
|
||||
public String getDiscordToken()
|
||||
{
|
||||
return mapping.string("discord-token");
|
||||
}
|
||||
|
||||
public String getLocalAddr()
|
||||
{
|
||||
return mapping.string("local-addr");
|
||||
}
|
||||
|
||||
public String getYtdlCommand()
|
||||
{
|
||||
String ret = mapping.string("ytdl-cmd");
|
||||
if (ret == null)
|
||||
ret = "/usr/bin/youtube-dl";
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,9 @@ import com.amihaiemil.eoyaml.YamlMapping;
|
|||
import com.amihaiemil.eoyaml.YamlSequence;
|
||||
import com.amihaiemil.eoyaml.YamlSequenceBuilder;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
|
@ -19,22 +22,33 @@ import java.util.List;
|
|||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class Song
|
||||
public class Track implements Comparable<Track>
|
||||
{
|
||||
private String title;
|
||||
private String artist;
|
||||
private final URL url;
|
||||
private File location = null;
|
||||
private InputStream inputStream = null;
|
||||
private int number;
|
||||
private List<Format> formats = new ArrayList<>();
|
||||
|
||||
private String requestedBy;
|
||||
private String requestedIn;
|
||||
private boolean kept = false;
|
||||
|
||||
public Song(URL url)
|
||||
private double progress = -1;
|
||||
private double eta = -1;
|
||||
|
||||
private final TrackRequest request;
|
||||
|
||||
public Track(URL url, TrackRequest request)
|
||||
{
|
||||
this.url = url;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public Track(TrackRequest request)
|
||||
{
|
||||
this.request = request;
|
||||
this.url = request.getUrl();
|
||||
}
|
||||
|
||||
public YamlMapping toYaml()
|
||||
|
@ -50,30 +64,30 @@ public class Song
|
|||
.add("location", location.getAbsolutePath())
|
||||
.add("num", Integer.toString(number))
|
||||
.add("formats", build.build())
|
||||
.add("requestedBy", requestedBy)
|
||||
.add("requestedIn", requestedIn)
|
||||
// .add("requestedBy", requestedBy)
|
||||
// .add("requestedIn", requestedIn)
|
||||
.add("kept", Boolean.toString(kept))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Song fromYaml(YamlMapping map) throws MalformedURLException
|
||||
public static Track fromYaml(YamlMapping map) throws MalformedURLException
|
||||
{
|
||||
Song song = new Song(new URL(map.string("url")));
|
||||
song.setArtist(map.string("artist"));
|
||||
song.setTitle(map.string("title"));
|
||||
song.setLocation(new File(map.string("location")));
|
||||
song.setNumber(map.integer("num"));
|
||||
song.setKept(Boolean.parseBoolean(map.string("kept")));
|
||||
song.setRequestedBy(map.string("requestedBy"));
|
||||
song.setRequestedIn(map.string("requestedIn"));
|
||||
Track track = new Track(new URL(map.string("url")), null);
|
||||
track.setArtist(map.string("artist"));
|
||||
track.setTitle(map.string("title"));
|
||||
track.setLocation(new File(map.string("location")));
|
||||
track.setNumber(map.integer("num"));
|
||||
track.setKept(Boolean.parseBoolean(map.string("kept")));
|
||||
// track.setRequestedBy(map.string("requestedBy"));
|
||||
// track.setRequestedIn(map.string("requestedIn"));
|
||||
|
||||
List<Format> formats = new ArrayList<>();
|
||||
YamlSequence formatSeq = map.yamlSequence("formats");
|
||||
for (int i = 0; i < formats.size(); i++)
|
||||
formats.add(Format.fromYaml(formatSeq.yamlMapping(i)));
|
||||
song.setFormats(formats);
|
||||
track.setFormats(formats);
|
||||
|
||||
return song;
|
||||
return track;
|
||||
}
|
||||
|
||||
public boolean isDownloaded()
|
||||
|
@ -126,6 +140,18 @@ public class Song
|
|||
this.location = location;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws FileNotFoundException
|
||||
{
|
||||
if (inputStream == null && location != null)
|
||||
inputStream = new FileInputStream(location);
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public void setInputStream(InputStream inputStream)
|
||||
{
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
void delete()
|
||||
{
|
||||
if (location != null)
|
||||
|
@ -133,26 +159,6 @@ public class Song
|
|||
location = null;
|
||||
}
|
||||
|
||||
public String getRequestedBy()
|
||||
{
|
||||
return requestedBy;
|
||||
}
|
||||
|
||||
public void setRequestedBy(String requestedBy)
|
||||
{
|
||||
this.requestedBy = requestedBy;
|
||||
}
|
||||
|
||||
public String getRequestedIn()
|
||||
{
|
||||
return requestedIn;
|
||||
}
|
||||
|
||||
public void setRequestedIn(String requestedIn)
|
||||
{
|
||||
this.requestedIn = requestedIn;
|
||||
}
|
||||
|
||||
public boolean isKept()
|
||||
{
|
||||
return kept;
|
||||
|
@ -163,6 +169,11 @@ public class Song
|
|||
this.kept = kept;
|
||||
}
|
||||
|
||||
public TrackRequest getRequest()
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
@ -197,4 +208,30 @@ public class Song
|
|||
return formats.get(0);
|
||||
}
|
||||
|
||||
public double getProgress()
|
||||
{
|
||||
return progress;
|
||||
}
|
||||
|
||||
public void setProgress(double progress)
|
||||
{
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public double getEta()
|
||||
{
|
||||
return eta;
|
||||
}
|
||||
|
||||
public void setEta(double eta)
|
||||
{
|
||||
this.eta = eta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Track o)
|
||||
{
|
||||
return Integer.compare(number, o.number);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,9 +5,12 @@
|
|||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
|
@ -24,6 +27,8 @@ public class TrackPlayer implements Closeable
|
|||
|
||||
private static final int DESIRED_BUFFER_SIZE = 3840 * 500;
|
||||
private static final int 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);
|
||||
|
||||
|
@ -31,10 +36,38 @@ public class TrackPlayer implements Closeable
|
|||
|
||||
private boolean arrayErr = false; // supresses ArrayIndexOutOfBoundsException after the first time, to prevent spam
|
||||
|
||||
public TrackPlayer(Song song) throws UnsupportedAudioFileException, IOException
|
||||
public TrackPlayer(Track track) throws UnsupportedAudioFileException, IOException
|
||||
{
|
||||
AudioInputStream in = AudioSystem.getAudioInputStream(song.getLocation());
|
||||
AudioFormat decodedFormat = AudioSendHandler.INPUT_FORMAT;
|
||||
AudioInputStream in = null;
|
||||
AudioFormat decodedFormat = null;
|
||||
int retry = 0;
|
||||
while (in == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
in = AudioSystem.getAudioInputStream(new BufferedInputStream(track.getInputStream()));
|
||||
decodedFormat = AudioSendHandler.INPUT_FORMAT;
|
||||
|
||||
break; // it worked!
|
||||
} catch (Exception ex)
|
||||
{
|
||||
retry++;
|
||||
if (retry < RETRY_COUNT)
|
||||
{
|
||||
System.err.println("Open file " + track.getLocation() + " failed because " + ex.getMessage() + " retry " + retry + "...");
|
||||
try
|
||||
{
|
||||
Thread.sleep(((long) Math.pow(2, retry)) * RETRY_DELAY);
|
||||
} catch (InterruptedException ex1)
|
||||
{
|
||||
}
|
||||
} else
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
input = AudioSystem.getAudioInputStream(decodedFormat, in);
|
||||
fillBuffer(false);
|
||||
}
|
||||
|
@ -58,7 +91,7 @@ public class TrackPlayer implements Closeable
|
|||
// throw new OutOfInputException();
|
||||
|
||||
int toRead = Math.min(length, audioBuffer.getCurrentNumberOfBytes());
|
||||
// System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes());
|
||||
System.out.println("To read: " + toRead + " from " + audioBuffer.getCurrentNumberOfBytes());
|
||||
if (toRead <= 0)
|
||||
throw new OutOfInputException();
|
||||
|
||||
|
@ -136,6 +169,7 @@ public class TrackPlayer implements Closeable
|
|||
}
|
||||
|
||||
|
||||
|
||||
public static class OutOfInputException extends RuntimeException
|
||||
{
|
||||
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Copyright (C) 2022 jimj316
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package moe.nekojimi.chords;
|
||||
|
||||
import com.amihaiemil.eoyaml.YamlMapping;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import moe.nekojimi.chords.commands.Invocation;
|
||||
import moe.nekojimi.musicsearcher.Result;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class TrackRequest implements Comparable<TrackRequest>
|
||||
{
|
||||
private Invocation invocation;
|
||||
|
||||
private String query;
|
||||
private URL url;
|
||||
|
||||
private Result result;
|
||||
|
||||
private final List<Track> tracks = new ArrayList<>();
|
||||
|
||||
private String requestedBy;
|
||||
private String requestedIn;
|
||||
|
||||
private double priority = 1.0;
|
||||
|
||||
public YamlMapping toYAML()
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
public static TrackRequest fromYAML(YamlMapping yaml)
|
||||
{
|
||||
TrackRequest ret = new TrackRequest();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Result getResult()
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(Result result)
|
||||
{
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public Invocation getInvocation()
|
||||
{
|
||||
return invocation;
|
||||
}
|
||||
|
||||
public void setInvocation(Invocation invocation)
|
||||
{
|
||||
this.invocation = invocation;
|
||||
requestedBy = invocation.getRequestMessage().getAuthor().getName();
|
||||
requestedIn = invocation.getRequestMessage().getChannel().getId();
|
||||
}
|
||||
|
||||
// public Message getRequestMessage()
|
||||
// {
|
||||
// return requestMessage;
|
||||
// }
|
||||
//
|
||||
// public void setRequestMessage(Message requestMessage)
|
||||
// {
|
||||
// this.requestMessage = requestMessage;
|
||||
// }
|
||||
//
|
||||
// public Message getResponseMessage()
|
||||
// {
|
||||
// return responseMessage;
|
||||
// }
|
||||
//
|
||||
// public void setResponseMessage(Message responseMessage)
|
||||
// {
|
||||
// this.responseMessage = responseMessage;
|
||||
// }
|
||||
|
||||
public String getQuery()
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(String query)
|
||||
{
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public URL getUrl()
|
||||
{
|
||||
if (url != null)
|
||||
return url;
|
||||
else if (result != null)
|
||||
return (result.getLink());
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setUrl(URL url)
|
||||
{
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public List<Track> getTracks()
|
||||
{
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public void addTrack(Track track)
|
||||
{
|
||||
tracks.add(track);
|
||||
}
|
||||
|
||||
public void clearTracks()
|
||||
{
|
||||
tracks.clear();
|
||||
}
|
||||
|
||||
public String getRequestedBy()
|
||||
{
|
||||
return requestedBy;
|
||||
}
|
||||
|
||||
public String getRequestedIn()
|
||||
{
|
||||
return requestedIn;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
String trackName;
|
||||
if (tracks.isEmpty())
|
||||
trackName = "Request";
|
||||
else
|
||||
{
|
||||
trackName = tracks.get(0).toString();
|
||||
if (tracks.size() > 1)
|
||||
trackName += " & " + (tracks.size() - 1) + " more tracks";
|
||||
}
|
||||
|
||||
String requestName = "";
|
||||
if (!requestedBy.isEmpty())
|
||||
requestName = "for " + requestedBy;
|
||||
|
||||
return (trackName + " " + requestName).trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int hash = 7;
|
||||
hash = 83 * hash + Objects.hashCode(this.url);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
final TrackRequest other = (TrackRequest) obj;
|
||||
if (!Objects.equals(this.url, other.url))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int compareTo(TrackRequest o)
|
||||
{
|
||||
return -Double.compare(priority, o.priority); // backwards so higher priority comes first
|
||||
}
|
||||
|
||||
public double getPriority()
|
||||
{
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(double priority)
|
||||
{
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package moe.nekojimi.chords;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2022 jimj316
|
||||
|
@ -25,6 +27,10 @@ import java.nio.ByteBuffer;
|
|||
*/
|
||||
public class Util
|
||||
{
|
||||
|
||||
private static final Pattern SIZE_PATTERN = Pattern.compile("\\b([0-9]+\\.?[0-9]*)([kkMmGg])i?[bB]\\b");
|
||||
private static final Pattern SAMPLE_RATE_PATTERN = Pattern.compile("\\b([0-9]+)k(b(ps?))?\\b");
|
||||
|
||||
public static String printSamples(ByteBuffer buf)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
@ -34,4 +40,56 @@ public class Util
|
|||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static int parseSampleRate(String input)
|
||||
{
|
||||
Matcher matcher = SAMPLE_RATE_PATTERN.matcher(input);
|
||||
if (matcher.find())
|
||||
{
|
||||
return Integer.parseInt(matcher.group(1)) * 1000;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static long parseSize(String note)
|
||||
{
|
||||
Matcher matcher = SIZE_PATTERN.matcher(note);
|
||||
if (matcher.find())
|
||||
{
|
||||
double value = Double.parseDouble(matcher.group(1));
|
||||
String mag = matcher.group(2).toUpperCase();
|
||||
long mult = 1;
|
||||
switch (mag)
|
||||
{
|
||||
case "K":
|
||||
mult = 1024;
|
||||
break;
|
||||
case "M":
|
||||
mult = 1024 * 1024;
|
||||
break;
|
||||
case "G":
|
||||
mult = 1024 * 1024 * 1024;
|
||||
break;
|
||||
}
|
||||
value *= mult;
|
||||
return (long) value;
|
||||
|
||||
}
|
||||
return -1L;
|
||||
}
|
||||
|
||||
public static String formatProgressBar(double progressPercent, String[] symbols, int barLength)
|
||||
{
|
||||
String ret = "";
|
||||
double filledSegments = (progressPercent / 100.0) * barLength;
|
||||
|
||||
for (int i = 0; i < barLength; i++)
|
||||
{
|
||||
double thisSegment = Math.min(1, Math.max(0, filledSegments - i));
|
||||
int symbolIdx = (int) Math.floor(thisSegment * symbols.length);
|
||||
String symbol = symbols[symbolIdx];
|
||||
ret += symbol;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -16,26 +14,35 @@ import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
|||
public abstract class Command
|
||||
{
|
||||
|
||||
protected final Main bot;
|
||||
protected final Chords bot;
|
||||
protected final String keyword;
|
||||
protected String documentation;
|
||||
|
||||
public Command(Main bot, String keyword)
|
||||
public Command(Chords bot, String keyword)
|
||||
{
|
||||
this.bot = bot;
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public abstract void call(GuildMessageReceivedEvent event, List<String> arg);
|
||||
public abstract void call(Invocation invocation);
|
||||
|
||||
public String getKeyword()
|
||||
{
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public String getDocumentation()
|
||||
public String argumentDescription()
|
||||
{
|
||||
return documentation;
|
||||
return ""; // most commands take no arguments
|
||||
}
|
||||
|
||||
public String synopsis()
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
//
|
||||
public String help()
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,35 +16,36 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
public class HelpCommand extends Command
|
||||
{
|
||||
|
||||
public HelpCommand(Main bot)
|
||||
public HelpCommand(Chords bot)
|
||||
{
|
||||
super(bot, "help");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
String help = "Commands available:\n"
|
||||
+ "!join <Channel> - Joins a voice channel\n"
|
||||
+ "!leave - Leaves the current voice channel\n"
|
||||
+ "!play <URL> - Downloads the track at that URL and adds it to the queue.\n"
|
||||
+ "!play <Search> - Searches for a track and displays a selection menu.\n"
|
||||
+ "!queue - Show the songs currently playing and the current queue.\n"
|
||||
+ "!remove <Index> - Remove the song at position <Index> from the queue.\n"
|
||||
+ "!skip - Skip the current song and play the next one.\n"
|
||||
+ "!restart - Try playing the current song again in case it goes wrong.\n";
|
||||
// for (String key: commands.keySet())
|
||||
// {
|
||||
// help += "!" + key + ":"
|
||||
// }
|
||||
event.getChannel().sendMessage(help).queue();
|
||||
+ "!queue - Show the tracks currently playing and the current queue.\n"
|
||||
+ "!remove <Index> - Remove the track at position <Index> from the queue.\n"
|
||||
+ "!skip - Skip the current track and play the next one.\n"
|
||||
+ "!restart - Try playing the current track again in case it goes wrong.\n";
|
||||
final Map<String, Command> commands = bot.getCommands();
|
||||
for (Entry<String, Command> e : commands.entrySet())
|
||||
{
|
||||
help += "!" + e.getKey() + " ".repeat(10 - e.getKey().length()) + "- " + e.getValue().synopsis();
|
||||
}
|
||||
invocation.respond(help);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,70 +14,52 @@
|
|||
* 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;
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import moe.nekojimi.musicsearcher.Result;
|
||||
import net.dv8tion.jda.api.MessageBuilder;
|
||||
import net.dv8tion.jda.api.entities.Message;
|
||||
import net.dv8tion.jda.api.requests.restaction.MessageAction;
|
||||
import net.dv8tion.jda.internal.entities.DataMessage;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author jimj316
|
||||
*/
|
||||
public class SongRequest
|
||||
public class Invocation
|
||||
{
|
||||
|
||||
private Message requestMessage;
|
||||
private Message responseMessage;
|
||||
|
||||
private String query;
|
||||
private URL url;
|
||||
private final MessageReceivedEvent event;
|
||||
private final List<String> args;
|
||||
|
||||
private List<Result> searchResults;
|
||||
public Invocation(MessageReceivedEvent event, List<String> args)
|
||||
{
|
||||
this.event = event;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
private Result result;
|
||||
public MessageReceivedEvent getEvent()
|
||||
{
|
||||
return event;
|
||||
}
|
||||
|
||||
private Song song;
|
||||
public List<String> getArgs()
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void respond(String text)
|
||||
{
|
||||
MessageAction action = null;
|
||||
|
||||
if (responseMessage == null)
|
||||
{
|
||||
action = requestMessage.reply(text);
|
||||
responseMessage = requestMessage.reply(text).complete();
|
||||
} else
|
||||
{
|
||||
action = responseMessage.editMessage(text);
|
||||
responseMessage = responseMessage.editMessage(text).complete();
|
||||
}
|
||||
|
||||
responseMessage = action.complete();
|
||||
}
|
||||
|
||||
|
||||
public List<Result> getSearchResults()
|
||||
{
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
public void setSearchResults(List<Result> searchResults)
|
||||
{
|
||||
this.searchResults = searchResults;
|
||||
}
|
||||
|
||||
public Result getResult()
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(Result result)
|
||||
{
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public Message getRequestMessage()
|
||||
|
@ -100,34 +82,4 @@ public class SongRequest
|
|||
this.responseMessage = responseMessage;
|
||||
}
|
||||
|
||||
public String getQuery()
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(String query)
|
||||
{
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public URL getUrl()
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(URL url)
|
||||
{
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Song getSong()
|
||||
{
|
||||
return song;
|
||||
}
|
||||
|
||||
public void setSong(Song song)
|
||||
{
|
||||
this.song = song;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,23 +6,26 @@
|
|||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
import net.dv8tion.jda.api.entities.*;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
|
||||
public class JoinCommand extends Command
|
||||
{
|
||||
|
||||
public JoinCommand(Main bot)
|
||||
public JoinCommand(Chords bot)
|
||||
{
|
||||
super(bot, "join");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> args)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
TextChannel textChannel = event.getChannel();
|
||||
MessageReceivedEvent event = invocation.getEvent();
|
||||
List<String> args = invocation.getArgs();
|
||||
MessageChannel textChannel = event.getChannel();
|
||||
VoiceChannel channel = null;
|
||||
if (args.isEmpty() || args.get(0).isBlank())
|
||||
{
|
||||
|
@ -30,8 +33,8 @@ public class JoinCommand extends Command
|
|||
if (member != null)
|
||||
{
|
||||
GuildVoiceState voiceState = member.getVoiceState();
|
||||
if (voiceState != null && voiceState.inVoiceChannel())
|
||||
channel = voiceState.getChannel();
|
||||
if (voiceState != null && voiceState.inAudioChannel())
|
||||
channel = voiceState.getChannel().asVoiceChannel();
|
||||
}
|
||||
if (channel == null)
|
||||
{
|
||||
|
@ -64,40 +67,20 @@ public class JoinCommand extends Command
|
|||
|
||||
if (channel == null) // I have no idea what you want mr user
|
||||
{
|
||||
onUnknownChannel(textChannel, arg0); // Let the user know about our failure
|
||||
return;
|
||||
// onUnknownChannel(textChannel, arg0); // Let the user know about our failure
|
||||
throw new RuntimeException("Unable to connect to \"" + channel.getName() + "\", no such channel!");
|
||||
}
|
||||
|
||||
}
|
||||
bot.connectTo(channel); // We found a channel to connect to!
|
||||
onConnecting(channel, textChannel); // Let the user know, we were successful!
|
||||
invocation.respond("Connecting to " + channel.getName() + "...");
|
||||
// onConnecting(channel, textChannel); // Let the user know, we were successful!
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@Override
|
||||
public String argumentDescription()
|
||||
{
|
||||
textChannel.sendMessage("Connecting to " + channel.getName()).queue(); // never forget to queue()!
|
||||
}
|
||||
|
||||
/**
|
||||
* The channel to connect to is not known to us.
|
||||
*
|
||||
* @param channel
|
||||
* The message channel (text channel abstraction) to send failure
|
||||
* information to
|
||||
* @param comment
|
||||
* The information of this channel
|
||||
*/
|
||||
public void onUnknownChannel(MessageChannel channel, String comment)
|
||||
{
|
||||
channel.sendMessage("Unable to connect to ``" + comment + "``, no such channel!").queue(); // never forget to queue()!
|
||||
return "<Channel?>";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,20 +5,18 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
public class LeaveCommand extends Command
|
||||
{
|
||||
|
||||
public LeaveCommand(Main bot)
|
||||
public LeaveCommand(Chords bot)
|
||||
{
|
||||
super(bot, "leave");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
if (bot.getCurrentVoiceChannel() != null)
|
||||
{
|
||||
|
|
|
@ -21,11 +21,10 @@ import java.net.URL;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import moe.nekojimi.chords.SongRequest;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
import moe.nekojimi.chords.TrackRequest;
|
||||
import moe.nekojimi.musicsearcher.Query;
|
||||
import moe.nekojimi.musicsearcher.Result;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -36,43 +35,43 @@ public class PlayCommand extends Command
|
|||
private static final double SEARCH_SCORE_THRESHOLD_DISPLAY = 0.6;
|
||||
private static final double SEARCH_SCORE_THRESHOLD_AUTOPLAY = 9999; // disable autoplay it sucks
|
||||
|
||||
// private List<Result> lastSearchResults;
|
||||
private List<Result> lastSearchResults;
|
||||
|
||||
public PlayCommand(Main main)
|
||||
public PlayCommand(Chords main)
|
||||
{
|
||||
super(main, "play");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
SongRequest request = new SongRequest();
|
||||
request.setRequestMessage(event.getMessage());
|
||||
TrackRequest request = new TrackRequest();
|
||||
request.setInvocation(invocation);
|
||||
try
|
||||
{
|
||||
final URL url = new URL(arg.get(0));
|
||||
final URL url = new URL(invocation.getArgs().get(0));
|
||||
request.setUrl(url);
|
||||
bot.queueDownload(request);
|
||||
|
||||
} catch (MalformedURLException mux)
|
||||
{
|
||||
// not a URL, try parsing it as a search result
|
||||
if (request.getSearchResults() != null && !request.getSearchResults().isEmpty())
|
||||
if (lastSearchResults != null && !lastSearchResults.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int index = Integer.parseInt(arg.get(0));
|
||||
int size = request.getSearchResults().size();
|
||||
int index = Integer.parseInt(invocation.getArgs().get(0));
|
||||
int size = lastSearchResults.size();
|
||||
if (index >= 1 && index <= size)
|
||||
{
|
||||
Result result = request.getSearchResults().get(index - 1);
|
||||
Result result = lastSearchResults.get(index - 1);
|
||||
request.setResult(result);
|
||||
bot.queueDownload(request);
|
||||
// event.getChannel().sendMessage("Song removed.").queue();
|
||||
// event.getChannel().sendMessage("Track removed.").queue();
|
||||
} else if (size > 1)
|
||||
event.getChannel().sendMessage("That's not a number between 1 and " + size + "!").queue();
|
||||
invocation.respond("That's not a number between 1 and " + size + "!");
|
||||
else if (size == 1)
|
||||
event.getChannel().sendMessage("There's only one song and that's not one of them!").queue();
|
||||
invocation.respond("There's only one track and that's not one of them!");
|
||||
|
||||
return;
|
||||
} catch (NumberFormatException nfx)
|
||||
|
@ -82,22 +81,21 @@ public class PlayCommand extends Command
|
|||
}
|
||||
|
||||
// otherwise, try searching
|
||||
CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(arg.stream().reduce((t, u) -> t + " " + u).get()));
|
||||
event.getChannel().sendMessage("Searching for \"" + arg + "\" ...").queue();
|
||||
CompletableFuture<List<Result>> search = bot.getSearcher().search(Query.fullText(invocation.getArgs().stream().reduce((t, u) -> t + " " + u).get()));
|
||||
invocation.respond("Searching for \"" + invocation.getArgs() + "\" ...");
|
||||
search.orTimeout(30, TimeUnit.SECONDS).whenCompleteAsync((List<Result> results, Throwable exec) ->
|
||||
{
|
||||
if (exec != null)
|
||||
{
|
||||
event.getChannel().sendMessage("Failed to search! Reason: " + exec.getMessage()).queue();
|
||||
invocation.respond("Failed to search! Reason: " + exec.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
request.setSearchResults(results);
|
||||
// lastSearchResults = results;
|
||||
lastSearchResults = results;
|
||||
|
||||
if (results.isEmpty())
|
||||
{
|
||||
event.getChannel().sendMessage("Found nothing! :(").queue();
|
||||
invocation.respond("Found nothing! :(");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -125,10 +123,15 @@ public class PlayCommand extends Command
|
|||
i++;
|
||||
}
|
||||
resultString += "Type eg. `!play 1` to select";
|
||||
event.getChannel().sendMessage(resultString).queue();
|
||||
invocation.respond(resultString);
|
||||
});
|
||||
// event.getChannel().sendMessage("That's not a valid URL you idiot! " + ex.getMessage()).queue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String argumentDescription()
|
||||
{
|
||||
return "<URL / Search>";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,57 +18,69 @@ package moe.nekojimi.chords.commands;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import moe.nekojimi.chords.Downloader;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import moe.nekojimi.chords.Song;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
import moe.nekojimi.chords.Track;
|
||||
import moe.nekojimi.chords.TrackRequest;
|
||||
|
||||
public class QueueCommand extends Command
|
||||
{
|
||||
|
||||
public QueueCommand(Main bot)
|
||||
public QueueCommand(Chords bot)
|
||||
{
|
||||
super(bot, "queue");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
String message = ">>> ";
|
||||
int i = 1;
|
||||
|
||||
final Song currentSong = bot.getMusicHandler().getCurrentSong();
|
||||
if (currentSong != null)
|
||||
message += ":notes: **Now playing: " + currentSong + "**\n";
|
||||
Track currentTrack = null;
|
||||
if (bot.getMusicHandler() != null)
|
||||
currentTrack = bot.getMusicHandler().getCurrentTrack();
|
||||
if (currentTrack != null)
|
||||
message += ":notes: **Now playing: " + currentTrack + "**\n";
|
||||
else
|
||||
message += ":mute: **Not playing anything right now.**\n";
|
||||
|
||||
final Queue<Song> songQueue = bot.getQueueManager().getJukeboxQueue();
|
||||
if (!songQueue.isEmpty())
|
||||
final Queue<Track> trackQueue = bot.getQueueManager().getJukeboxQueue();
|
||||
if (!trackQueue.isEmpty())
|
||||
{
|
||||
message += "__Ready to play:__\n";
|
||||
for (Song song : songQueue)
|
||||
for (Track track : trackQueue)
|
||||
{
|
||||
message += ":bread: **" + (i) + ":** " + song + "\n";
|
||||
message += ":bread: **" + i + ":** " + track + "\n";
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
final List<Downloader.DownloadTask> downloadQueue = bot.getDownloader().getDownloadQueue();
|
||||
if (!downloadQueue.isEmpty())
|
||||
final List<TrackRequest> downloading = bot.getDownloader().getDownloadingTracks();
|
||||
if (!downloading.isEmpty())
|
||||
{
|
||||
message += "__Downloading:__\n";
|
||||
for (Downloader.DownloadTask task : downloadQueue)
|
||||
for (TrackRequest request : downloading)
|
||||
{
|
||||
message += ":inbox_tray: **" + (i) + ":** " + task.getSong() + "\n";
|
||||
message += ":satellite: **" + (i) + ":** " + request + "\n";
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadQueue.isEmpty() && songQueue.isEmpty())
|
||||
final List<TrackRequest> downloadQueue = bot.getDownloader().getDownloadQueue();
|
||||
if (!downloadQueue.isEmpty())
|
||||
{
|
||||
message += "__In queue for download:__\n";
|
||||
for (TrackRequest request : downloadQueue)
|
||||
{
|
||||
message += ":inbox_tray: **" + (i) + ":** " + request + "\n";
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloading.isEmpty() && trackQueue.isEmpty() && downloadQueue.isEmpty())
|
||||
message += ":mailbox_with_no_mail: The track queue is empty.";
|
||||
// :inbox_tray:
|
||||
event.getChannel().sendMessage(message).queue();
|
||||
invocation.respond(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,36 +16,34 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
public class RemoveCommand extends Command
|
||||
{
|
||||
|
||||
public RemoveCommand(Main bot)
|
||||
public RemoveCommand(Chords bot)
|
||||
{
|
||||
super(bot, "remove");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
try
|
||||
{
|
||||
int i = Integer.parseInt(arg.get(0));
|
||||
boolean removed = bot.getQueueManager().removeSong(i - 1);
|
||||
int i = Integer.parseInt(invocation.getArgs().get(0));
|
||||
boolean removed = bot.getQueueManager().removeTrack(i - 1);
|
||||
final int size = bot.getQueueManager().getJukeboxQueue().size();
|
||||
if (removed)
|
||||
event.getChannel().sendMessage("Song removed.").queue();
|
||||
invocation.respond("Track removed.");
|
||||
else if (size > 1)
|
||||
event.getChannel().sendMessage("That's not a number between 1 and " + size + "!").queue();
|
||||
invocation.respond("That's not a number between 1 and " + size + "!");
|
||||
else if (size == 1)
|
||||
event.getChannel().sendMessage("There's only one song to remove and that's not one of them!").queue();
|
||||
invocation.respond("There's only one track to remove and that's not one of them!");
|
||||
|
||||
} catch (NumberFormatException ex)
|
||||
{
|
||||
event.getChannel().sendMessage(arg + " isn't a number!").queue();
|
||||
invocation.respond(invocation.getArgs().get(0) + " isn't a number!");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,27 +16,25 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
public class RestartCommand extends Command
|
||||
{
|
||||
|
||||
public RestartCommand(Main bot)
|
||||
public RestartCommand(Chords bot)
|
||||
{
|
||||
super(bot, "restart");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
// TODO: this needs to clear the current data queue
|
||||
boolean ok = bot.getQueueManager().restartSong();
|
||||
boolean ok = bot.getQueueManager().restartTrack();
|
||||
if (ok)
|
||||
event.getChannel().sendMessage("Restarted current song!").queue();
|
||||
invocation.respond("Restarted current track!");
|
||||
else
|
||||
event.getChannel().sendMessage("Cannot restart!").queue();
|
||||
invocation.respond("Cannot restart!");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,26 +16,24 @@
|
|||
*/
|
||||
package moe.nekojimi.chords.commands;
|
||||
|
||||
import java.util.List;
|
||||
import moe.nekojimi.chords.Main;
|
||||
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
|
||||
import moe.nekojimi.chords.Chords;
|
||||
|
||||
public class SkipCommand extends Command
|
||||
{
|
||||
|
||||
public SkipCommand(Main bot)
|
||||
public SkipCommand(Chords bot)
|
||||
{
|
||||
super(bot, "skip");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void call(GuildMessageReceivedEvent event, List<String> arg)
|
||||
public void call(Invocation invocation)
|
||||
{
|
||||
boolean ok = bot.getMusicHandler().nextSong(true);
|
||||
boolean ok = bot.getMusicHandler().skipTrack();
|
||||
if (ok)
|
||||
event.getChannel().sendMessage("Skipped to next song!").queue();
|
||||
invocation.respond("Skipped to next track!");
|
||||
else
|
||||
event.getChannel().sendMessage("There's no more songs!").queue();
|
||||
invocation.respond("There's no more tracks!");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue