|
|
|
/*
|
|
|
|
* 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.beust.jcommander.Parameter;
|
|
|
|
import com.neovisionaries.ws.client.WebSocketFactory;
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.FilenameFilter;
|
|
|
|
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.net.SocketFactory;
|
|
|
|
import javax.net.ssl.SSLSocketFactory;
|
|
|
|
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 static final CommandOptions options = new CommandOptions();
|
|
|
|
|
|
|
|
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
|
|
|
|
{
|
|
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
|
|
JDABuilder builder = JDABuilder.createDefault(options.getToken(), 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);
|
|
|
|
|
|
|
|
if (options.getLocalAddress() != null)
|
|
|
|
{
|
|
|
|
final WebSocketFactory webSocketFactory = new WebSocketFactory();
|
|
|
|
final LocalBindSocketFactory localBindSocketFactory = new LocalBindSocketFactory();
|
|
|
|
localBindSocketFactory.setLocalAddress(InetAddress.getByName(options.getLocalAddress()));
|
|
|
|
webSocketFactory.setSocketFactory(localBindSocketFactory);
|
|
|
|
builder.setWebsocketFactory(webSocketFactory);
|
|
|
|
}
|
|
|
|
|
|
|
|
JDA jda = builder.build();
|
|
|
|
listener.setJda(jda);
|
|
|
|
}
|
|
|
|
private final Consumer<Song> nowPlayingConsumer = new NowPlayingConsumer();
|
|
|
|
|
|
|
|
private final BiConsumer<SongRequest, Exception> downloaderMessageHandler = new DownloaderMessageHandler();
|
|
|
|
|
|
|
|
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(null);
|
|
|
|
} catch (MalformedURLException ex)
|
|
|
|
{
|
|
|
|
// not a URL, then
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
String[] split = content.split("\\s+");
|
|
|
|
String cmd = split[0].toLowerCase();
|
|
|
|
|
|
|
|
if (!cmd.startsWith("!"))
|
|
|
|
return; // doesn't start with prefix char
|
|
|
|
|
|
|
|
cmd = cmd.substring(1); // strip prefix char
|
|
|
|
|
|
|
|
// String arg = "";
|
|
|
|
List<String> args = new ArrayList<>();
|
|
|
|
Collections.addAll(args, split);
|
|
|
|
args.remove(0);
|
|
|
|
|
|
|
|
Invocation invocation = new Invocation(event, args);
|
|
|
|
invocation.setRequestMessage(message);
|
|
|
|
|
|
|
|
if (commands.containsKey(cmd))
|
|
|
|
{
|
|
|
|
Command command = commands.get(cmd);
|
|
|
|
command.call(invocation);
|
|
|
|
} else
|
|
|
|
{
|
|
|
|
helpCommand.call(invocation);
|
|
|
|
}
|
|
|
|
} 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.getInvocation().getRequestMessage() != null)
|
|
|
|
{
|
|
|
|
song.setRequestedBy(request.getInvocation().getRequestMessage().getAuthor().getName());
|
|
|
|
song.setRequestedIn(request.getInvocation().getRequestMessage().getChannel().getId());
|
|
|
|
}
|
|
|
|
song.setNumber(trackNumber);
|
|
|
|
trackNumber++;
|
|
|
|
request.setSong(song);
|
|
|
|
downloader.accept(new Downloader.DownloadTask(request, queueManager));
|
|
|
|
request.getInvocation().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;
|
|
|
|
}
|
|
|
|
|
|
|
|
private class DownloaderMessageHandler implements BiConsumer<SongRequest, Exception>
|
|
|
|
{
|
|
|
|
|
|
|
|
public DownloaderMessageHandler()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void accept(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.getInvocation().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.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 + ")";
|
|
|
|
}
|
|
|
|
request.getInvocation().respond("Now downloading " + song + formatDetails + " ...");
|
|
|
|
log("DOWN", "Downloading " + song + "...");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
request.getInvocation().respond("Failed to download " + song + "! Reason: " + ex.getMessage());
|
|
|
|
log("DOWN", "Failed to download " + song + "! Reason: " + ex.getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class NowPlayingConsumer implements Consumer<Song>
|
|
|
|
{
|
|
|
|
|
|
|
|
public NowPlayingConsumer()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void accept(Song song)
|
|
|
|
{
|
|
|
|
if (song != null)
|
|
|
|
jda.getPresence().setActivity(Activity.of(Activity.ActivityType.LISTENING, song.toString()));
|
|
|
|
else
|
|
|
|
jda.getPresence().setActivity(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|