You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
556 lines
18 KiB
556 lines
18 KiB
/*
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
}
|
|
|