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

484 lines
16 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.entities.channel.unions.AudioChannelUnion;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
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 JDA jda;
private final Map<String, Command> commands = new HashMap<>();
private final Command helpCommand;
private PlayCommand playCommand;
private AudioChannel 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,
GatewayIntent.MESSAGE_CONTENT
);
settings = new Settings(new File(options.getSettingsPath()));
JDABuilder builder = JDABuilder.createDefault(settings.getDiscordToken(), 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 Chords listener = new Chords();
builder.addEventListeners(listener);
builder.setAutoReconnect(true);
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);
}
JDA jda = builder.build();
listener.setJda(jda);
}
private final Consumer<Song> nowPlayingConsumer = new NowPlayingConsumer();
private final BiConsumer<SongRequest, 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();
// 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 AudioChannelUnion channelLeft = event.getChannelLeft();
if (channelLeft.getMembers().isEmpty())
if (channelLeft == currentVoiceChannel)
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);
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(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 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 int getTrackNumber()
{
return trackNumber;
}
public static Settings getSettings()
{
return settings;
}
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);
}
}
}