Discord bot that plays music from every website ever via youtube-dl
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.
Chords/src/main/java/moe/nekojimi/chords/Chords.java

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);
}
}
}