diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..df1968a --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +jmusicbot.com \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ec01e4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + com.jagrosh + JMusicBot + Snapshot + jar + + + + dv8tion + m2-dv8tion + https://m2.dv8tion.net/releases + + + central + bintray + https://jcenter.bintray.com + + + jitpack.io + https://jitpack.io + + true + always + + + + m2.duncte123.dev + m2-duncte123 + https://m2.duncte123.dev/releases + + + arbjergDev + Lavalink Repository + https://maven.lavalink.dev/releases + + + + + + + net.dv8tion + JDA + 4.4.1_353 + + + com.github.JDA-Applications + JDA-Utilities + c16a4b264b + + + + + dev.arbjerg + lavaplayer + 2.2.2 + + + dev.arbjerg + lavaplayer-ext-youtube-rotator + 2.2.2 + + + dev.lavalink.youtube + common + 1.11.3 + + + com.github.jagrosh + JLyrics + master-SNAPSHOT + + + com.dunctebot + sourcemanagers + 1.9.0 + + + + + ch.qos.logback + logback-classic + 1.2.13 + + + com.typesafe + config + 1.3.2 + + + org.jsoup + jsoup + 1.15.3 + + + + + junit + junit + 4.13.1 + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.5 + + + package + + shade + + + true + All + + + *:* + + + + + reference.conf + + + + com.jagrosh.jmusicbot.JMusicBot + ${project.artifactId} + ${project.version} + ${project.artifactId} + ${project.version} + ${project.groupId} + + + + + + + + + + + + UTF-8 + 11 + 11 + + diff --git a/scripts/run_jmusicbot.sh b/scripts/run_jmusicbot.sh new file mode 100644 index 0000000..d51c056 --- /dev/null +++ b/scripts/run_jmusicbot.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# This will have this script check for a new version of JMusicBot every +# startup (and download it if the latest version isn't currently downloaded) +DOWNLOAD=true + +# This will cause the script to run in a loop so that the bot auto-restarts +# when you use the shutdown command +LOOP=true + +download() { + if [ $DOWNLOAD = true ]; then + URL=$(curl -s https://api.github.com/repos/jagrosh/MusicBot/releases/latest \ + | grep -i "browser_download_url.*\.jar" \ + | sed 's/.*\(http.*\)"/\1/') + FILENAME=$(echo $URL | sed 's/.*\/\([^\/]*\)/\1/') + if [ -f $FILENAME ]; then + echo "Latest version already downloaded (${FILENAME})" + else + curl -L $URL -o $FILENAME + fi + fi +} + +run() { + java -Dnogui=true -jar $(ls -t JMusicBot* | head -1) +} + +while + download + run + $LOOP +do + continue +done diff --git a/src/main/java/com/jagrosh/jmusicbot/Bot.java b/src/main/java/com/jagrosh/jmusicbot/Bot.java new file mode 100644 index 0000000..df0aaf3 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/Bot.java @@ -0,0 +1,221 @@ +package com.jagrosh.jmusicbot; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import com.jagrosh.jmusicbot.audio.AloneInVoiceHandler; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.NowplayingHandler; +import com.jagrosh.jmusicbot.audio.PlayerManager; +import com.jagrosh.jmusicbot.gui.GUI; +import com.jagrosh.jmusicbot.playlist.PlaylistLoader; +import com.jagrosh.jmusicbot.settings.SettingsManager; +import java.util.Objects; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Guild; +import java.io.*; +import java.nio.file.*; +import java.util.regex.*; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Bot +{ + private final EventWaiter waiter; + private final ScheduledExecutorService threadpool; + private final BotConfig config; + private final SettingsManager settings; + private final PlayerManager players; + private final PlaylistLoader playlists; + private final NowplayingHandler nowplaying; + private final AloneInVoiceHandler aloneInVoiceHandler; + + private boolean shuttingDown = false; + private JDA jda; + private GUI gui; + + public Bot(EventWaiter waiter, BotConfig config, SettingsManager settings) + { + this.waiter = waiter; + this.config = config; + this.settings = settings; + this.playlists = new PlaylistLoader(config); + this.threadpool = Executors.newSingleThreadScheduledExecutor(); + + //Update config.txt before init + updateConfig(); + + this.players = new PlayerManager(this, config); + this.players.init(); + this.nowplaying = new NowplayingHandler(this); + this.nowplaying.init(); + this.aloneInVoiceHandler = new AloneInVoiceHandler(this); + this.aloneInVoiceHandler.init(); + } + + public BotConfig getConfig() + { + return config; + } + + public SettingsManager getSettingsManager() + { + return settings; + } + + public EventWaiter getWaiter() + { + return waiter; + } + + public ScheduledExecutorService getThreadpool() + { + return threadpool; + } + + public PlayerManager getPlayerManager() + { + return players; + } + + public PlaylistLoader getPlaylistLoader() + { + return playlists; + } + + public NowplayingHandler getNowplayingHandler() + { + return nowplaying; + } + + public AloneInVoiceHandler getAloneInVoiceHandler() + { + return aloneInVoiceHandler; + } + + public JDA getJDA() + { + return jda; + } + + public void closeAudioConnection(long guildId) + { + Guild guild = jda.getGuildById(guildId); + if(guild!=null) + threadpool.submit(() -> guild.getAudioManager().closeAudioConnection()); + } + + public void resetGame() + { + Activity game = config.getGame()==null || config.getGame().getName().equalsIgnoreCase("none") ? null : config.getGame(); + if(!Objects.equals(jda.getPresence().getActivity(), game)) + jda.getPresence().setActivity(game); + } + + public void shutdown() + { + if(shuttingDown) + return; + shuttingDown = true; + threadpool.shutdownNow(); + if(jda.getStatus()!=JDA.Status.SHUTTING_DOWN) + { + jda.getGuilds().stream().forEach(g -> + { + g.getAudioManager().closeAudioConnection(); + AudioHandler ah = (AudioHandler)g.getAudioManager().getSendingHandler(); + if(ah!=null) + { + ah.stopAndClear(); + ah.getPlayer().destroy(); + } + }); + jda.shutdown(); + } + if(gui!=null) + gui.dispose(); + System.exit(0); + } + + public void setJDA(JDA jda) + { + this.jda = jda; + } + + public void setGUI(GUI gui) + { + this.gui = gui; + } + + private void updateConfig() + { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); + String currentTime = sdf.format(new Date()); + + try { + Process process = new ProcessBuilder("docker", "run", "quay.io/invidious/youtube-trusted-session-generator") + .redirectErrorStream(true) + .start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + process.waitFor(); + + String dockerOutput = output.toString(); + Pattern poTokenPattern = Pattern.compile("po_token:\\s*([^\\s]+)"); + Pattern visitorDataPattern = Pattern.compile("visitor_data:\\s*([^\\s]+)"); + + Matcher poTokenMatcher = poTokenPattern.matcher(dockerOutput); + Matcher visitorDataMatcher = visitorDataPattern.matcher(dockerOutput); + + if (poTokenMatcher.find() && visitorDataMatcher.find()) { + String poToken = poTokenMatcher.group(1); + String visitorData = visitorDataMatcher.group(1); + + Path configPath = Paths.get("config.txt"); + String configContent = Files.readString(configPath); + + if (!configContent.contains("ytpotoken =")) { + configContent += "\nytpotoken = \"" + poToken + "\""; + } else { + configContent = configContent.replaceAll("ytpotoken\\s*=\\s*\"[^\"]*\"", "ytpotoken = \"" + poToken + "\""); + } + + if (!configContent.contains("//New PO_TOKEN generated at:")) { + configContent += "\n//New PO_TOKEN generated at: " + currentTime; + } else { + configContent = configContent.replaceAll("(?<=ytpotoken\\s*=\\s*\"[^\"]*\")[^/]*(?=//New PO_TOKEN generated at:)", ""); + configContent = configContent.replaceAll("(?<=ytpotoken\\s*=\\s*\"[^\"]*\")", "//New PO_TOKEN generated at: " + currentTime); + } + + if (!configContent.contains("ytvisitordata =")) { + configContent += "\nytvisitordata = \"" + visitorData + "\""; + } else { + configContent = configContent.replaceAll("ytvisitordata\\s*=\\s*\"[^\"]*\"", "ytvisitordata = \"" + visitorData + "\""); + } + + if (!configContent.contains("//New VISITOR_DATA generated at:")) { + configContent += "\n//New VISITOR_DATA generated at: " + currentTime; + } else { + configContent = configContent.replaceAll("(?<=ytvisitordata\\s*=\\s*\"[^\"]*\")[^/]*(?=//New VISITOR_DATA generated at:)", ""); + configContent = configContent.replaceAll("(?<=ytvisitordata\\s*=\\s*\"[^\"]*\")", "//New VISITOR_DATA generated at: " + currentTime); + } + + Files.writeString(configPath, configContent); + + System.out.println("[" + currentTime + "] [INFO] Config.txt succesfully updated!"); + System.out.println("[" + currentTime + "] [INFO] ytpotoken = " + poToken); + System.out.println("[" + currentTime + "] [INFO] ytvisitordata = " + visitorData); + } else { + System.err.println("[" + currentTime + "] [ERROR]: Failed to find po_token or visitor_data in Docker result."); + } + } catch (Exception e) { + System.err.println("[" + currentTime + "] [ERROR]: Error while updating config.txt " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java new file mode 100644 index 0000000..9f4de83 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java @@ -0,0 +1,394 @@ +package com.jagrosh.jmusicbot; + +import com.jagrosh.jmusicbot.entities.Prompt; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.jagrosh.jmusicbot.utils.YouTubeUtil; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock; +import com.typesafe.config.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; + +public class BotConfig +{ + private final Prompt prompt; + private final static String CONTEXT = "Config"; + private final static String START_TOKEN = "/// START OF JMUSICBOT CONFIG ///"; + private final static String END_TOKEN = "/// END OF JMUSICBOT CONFIG ///"; + + private Path path = null; + private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel, + successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji, + ytPoToken, ytVisitorData, evalEngine; + private YouTubeUtil.RoutingPlanner ytRoutingPlanner; + private List ytIpBlocks; + private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots; + private long owner, maxSeconds, aloneTimeUntilStop; + private int maxYTPlaylistPages; + private double skipratio; + private OnlineStatus status; + private Activity game; + private Config aliases, transforms; + + private boolean valid = false; + + public BotConfig(Prompt prompt) + { + this.prompt = prompt; + } + + public void load() + { + valid = false; + + // read config from file + try + { + // get the path to the config, default config.txt + path = getConfigPath(); + + // load in the config file, plus the default values + //Config config = ConfigFactory.parseFile(path.toFile()).withFallback(ConfigFactory.load()); + Config config = ConfigFactory.load(); + + // set values + token = config.getString("token"); + prefix = config.getString("prefix"); + altprefix = config.getString("altprefix"); + helpWord = config.getString("help"); + owner = config.getLong("owner"); + successEmoji = config.getString("success"); + warningEmoji = config.getString("warning"); + errorEmoji = config.getString("error"); + loadingEmoji = config.getString("loading"); + searchingEmoji = config.getString("searching"); + game = OtherUtil.parseGame(config.getString("game")); + status = OtherUtil.parseStatus(config.getString("status")); + stayInChannel = config.getBoolean("stayinchannel"); + songInGame = config.getBoolean("songinstatus"); + npImages = config.getBoolean("npimages"); + updatealerts = config.getBoolean("updatealerts"); + logLevel = config.getString("loglevel"); + useEval = config.getBoolean("eval"); + evalEngine = config.getString("evalengine"); + maxSeconds = config.getLong("maxtime"); + maxYTPlaylistPages = config.getInt("maxytplaylistpages"); + aloneTimeUntilStop = config.getLong("alonetimeuntilstop"); + playlistsFolder = config.getString("playlistsfolder"); + aliases = config.getConfig("aliases"); + ytPoToken = config.getString("ytpotoken"); + ytVisitorData = config.getString("ytvisitordata"); + ytRoutingPlanner = config.getEnum(YouTubeUtil.RoutingPlanner.class, "ytroutingplanner"); + ytIpBlocks = config.getStringList("ytipblocks").stream().map(YouTubeUtil::parseIpBlock).collect(Collectors.toList()); + transforms = config.getConfig("transforms"); + skipratio = config.getDouble("skipratio"); + dbots = owner == 113156185389092864L; + + // we may need to write a new config file + boolean write = false; + + // validate bot token + if(token==null || token.isEmpty() || token.equalsIgnoreCase("BOT_TOKEN_HERE")) + { + token = prompt.prompt("Please provide a bot token." + + "\nInstructions for obtaining a token can be found here:" + + "\nhttps://github.com/jagrosh/MusicBot/wiki/Getting-a-Bot-Token." + + "\nBot Token: "); + if(token==null) + { + prompt.alert(Prompt.Level.WARNING, CONTEXT, "No token provided! Exiting.\n\nConfig Location: " + path.toAbsolutePath().toString()); + return; + } + else + { + write = true; + } + } + + // validate bot owner + if(owner<=0) + { + try + { + owner = Long.parseLong(prompt.prompt("Owner ID was missing, or the provided owner ID is not valid." + + "\nPlease provide the User ID of the bot's owner." + + "\nInstructions for obtaining your User ID can be found here:" + + "\nhttps://github.com/jagrosh/MusicBot/wiki/Finding-Your-User-ID" + + "\nOwner User ID: ")); + } + catch(NumberFormatException | NullPointerException ex) + { + owner = 0; + } + if(owner<=0) + { + prompt.alert(Prompt.Level.ERROR, CONTEXT, "Invalid User ID! Exiting.\n\nConfig Location: " + path.toAbsolutePath().toString()); + return; + } + else + { + write = true; + } + } + + if(write) + writeToFile(); + + // if we get through the whole config, it's good to go + valid = true; + } + catch (ConfigException ex) + { + prompt.alert(Prompt.Level.ERROR, CONTEXT, ex + ": " + ex.getMessage() + "\n\nConfig Location: " + path.toAbsolutePath().toString()); + } + } + + private void writeToFile() + { + byte[] bytes = loadDefaultConfig().replace("BOT_TOKEN_HERE", token) + .replace("0 // OWNER ID", Long.toString(owner)) + .trim().getBytes(); + try + { + Files.write(path, bytes); + } + catch(IOException ex) + { + prompt.alert(Prompt.Level.WARNING, CONTEXT, "Failed to write new config options to config.txt: "+ex + + "\nPlease make sure that the files are not on your desktop or some other restricted area.\n\nConfig Location: " + + path.toAbsolutePath().toString()); + } + } + + private static String loadDefaultConfig() + { + String original = OtherUtil.loadResource(new JMusicBot(), "/reference.conf"); + return original==null + ? "token = BOT_TOKEN_HERE\r\nowner = 0 // OWNER ID" + : original.substring(original.indexOf(START_TOKEN)+START_TOKEN.length(), original.indexOf(END_TOKEN)).trim(); + } + + private static Path getConfigPath() + { + Path path = OtherUtil.getPath(System.getProperty("config.file", System.getProperty("config", "config.txt"))); + if(path.toFile().exists()) + { + if(System.getProperty("config.file") == null) + System.setProperty("config.file", System.getProperty("config", path.toAbsolutePath().toString())); + ConfigFactory.invalidateCaches(); + } + return path; + } + + public static void writeDefaultConfig() + { + Prompt prompt = new Prompt(null, null, true, true); + prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Generating default config file"); + Path path = BotConfig.getConfigPath(); + byte[] bytes = BotConfig.loadDefaultConfig().getBytes(); + try + { + prompt.alert(Prompt.Level.INFO, "JMusicBot Config", "Writing default config file to " + path.toAbsolutePath().toString()); + Files.write(path, bytes); + } + catch(Exception ex) + { + prompt.alert(Prompt.Level.ERROR, "JMusicBot Config", "An error occurred writing the default config file: " + ex.getMessage()); + } + } + + public boolean isValid() + { + return valid; + } + + public String getConfigLocation() + { + return path.toFile().getAbsolutePath(); + } + + public String getPrefix() + { + return prefix; + } + + public String getAltPrefix() + { + return "NONE".equalsIgnoreCase(altprefix) ? null : altprefix; + } + + public String getToken() + { + return token; + } + + public double getSkipRatio() + { + return skipratio; + } + + public long getOwnerId() + { + return owner; + } + + public String getSuccess() + { + return successEmoji; + } + + public String getWarning() + { + return warningEmoji; + } + + public String getError() + { + return errorEmoji; + } + + public String getLoading() + { + return loadingEmoji; + } + + public String getSearching() + { + return searchingEmoji; + } + + public Activity getGame() + { + return game; + } + + public boolean isGameNone() + { + return game != null && game.getName().equalsIgnoreCase("none"); + } + + public OnlineStatus getStatus() + { + return status; + } + + public String getHelp() + { + return helpWord; + } + + public boolean getStay() + { + return stayInChannel; + } + + public boolean getSongInStatus() + { + return songInGame; + } + + public String getPlaylistsFolder() + { + return playlistsFolder; + } + + public boolean getDBots() + { + return dbots; + } + + public boolean useUpdateAlerts() + { + return updatealerts; + } + + public String getLogLevel() + { + return logLevel; + } + + public String getYTPoToken() + { + return ytPoToken.equals("PO_TOKEN_HERE") ? null : ytPoToken; + } + + public String getYTVisitorData() + { + return ytVisitorData.equals("VISITOR_DATA_HERE") ? null : ytVisitorData; + } + + public YouTubeUtil.RoutingPlanner getYTRoutingPlanner() + { + return ytRoutingPlanner; + } + + public List getYTIpBlocks() + { + return ytIpBlocks; + } + + public boolean useEval() + { + return useEval; + } + + public String getEvalEngine() + { + return evalEngine; + } + + public boolean useNPImages() + { + return npImages; + } + + public long getMaxSeconds() + { + return maxSeconds; + } + + public int getMaxYTPlaylistPages() + { + return maxYTPlaylistPages; + } + + public String getMaxTime() + { + return TimeUtil.formatTime(maxSeconds * 1000); + } + + public long getAloneTimeUntilStop() + { + return aloneTimeUntilStop; + } + + public boolean isTooLong(AudioTrack track) + { + if(maxSeconds<=0) + return false; + return Math.round(track.getDuration()/1000.0) > maxSeconds; + } + + public String[] getAliases(String command) + { + try + { + return aliases.getStringList(command).toArray(new String[0]); + } + catch(NullPointerException | ConfigException.Missing e) + { + return new String[0]; + } + } + + public Config getTransforms() + { + return transforms; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java b/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java new file mode 100644 index 0000000..d07573d --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/JMusicBot.java @@ -0,0 +1,232 @@ +package com.jagrosh.jmusicbot; + +import com.jagrosh.jdautilities.command.CommandClient; +import com.jagrosh.jdautilities.command.CommandClientBuilder; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import com.jagrosh.jdautilities.examples.command.*; +import com.jagrosh.jmusicbot.commands.admin.*; +import com.jagrosh.jmusicbot.commands.dj.*; +import com.jagrosh.jmusicbot.commands.general.*; +import com.jagrosh.jmusicbot.commands.music.*; +import com.jagrosh.jmusicbot.commands.owner.*; +import com.jagrosh.jmusicbot.entities.Prompt; +import com.jagrosh.jmusicbot.gui.GUI; +import com.jagrosh.jmusicbot.settings.SettingsManager; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import java.awt.Color; +import java.util.Arrays; +import javax.security.auth.login.LoginException; +import net.dv8tion.jda.api.*; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ch.qos.logback.classic.Level; + +public class JMusicBot +{ + public final static Logger LOG = LoggerFactory.getLogger(JMusicBot.class); + public final static Permission[] RECOMMENDED_PERMS = {Permission.MESSAGE_READ, Permission.MESSAGE_WRITE, Permission.MESSAGE_HISTORY, Permission.MESSAGE_ADD_REACTION, + Permission.MESSAGE_EMBED_LINKS, Permission.MESSAGE_ATTACH_FILES, Permission.MESSAGE_MANAGE, Permission.MESSAGE_EXT_EMOJI, + Permission.VOICE_CONNECT, Permission.VOICE_SPEAK, Permission.NICKNAME_CHANGE}; + public final static GatewayIntent[] INTENTS = {GatewayIntent.DIRECT_MESSAGES, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MESSAGE_REACTIONS, GatewayIntent.GUILD_VOICE_STATES}; + + /** + * @param args the command line arguments + */ + public static void main(String[] args) + { + if(args.length > 0) + switch(args[0].toLowerCase()) + { + case "generate-config": + BotConfig.writeDefaultConfig(); + return; + default: + } + startBot(); + } + + private static void startBot() + { + // create prompt to handle startup + Prompt prompt = new Prompt("JMusicBot"); + + // startup checks + OtherUtil.checkVersion(prompt); + OtherUtil.checkJavaVersion(prompt); + + // load config + BotConfig config = new BotConfig(prompt); + config.load(); + if(!config.isValid()) + return; + LOG.info("Loaded config from " + config.getConfigLocation()); + + // set log level from config + ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel( + Level.toLevel(config.getLogLevel(), Level.INFO)); + + // set up the listener + EventWaiter waiter = new EventWaiter(); + SettingsManager settings = new SettingsManager(); + Bot bot = new Bot(waiter, config, settings); + CommandClient client = createCommandClient(config, settings, bot); + + + if(!prompt.isNoGUI()) + { + try + { + GUI gui = new GUI(bot); + bot.setGUI(gui); + gui.init(); + + LOG.info("Loaded config from " + config.getConfigLocation()); + } + catch(Exception e) + { + LOG.error("Could not start GUI. If you are " + + "running on a server or in a location where you cannot display a " + + "window, please run in nogui mode using the -Dnogui=true flag."); + } + } + + // attempt to log in and start + try + { + JDA jda = JDABuilder.create(config.getToken(), Arrays.asList(INTENTS)) + .enableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE) + .disableCache(CacheFlag.ACTIVITY, CacheFlag.CLIENT_STATUS, CacheFlag.EMOTE, CacheFlag.ONLINE_STATUS) + .setActivity(config.isGameNone() ? null : Activity.playing("loading...")) + .setStatus(config.getStatus()==OnlineStatus.INVISIBLE || config.getStatus()==OnlineStatus.OFFLINE + ? OnlineStatus.INVISIBLE : OnlineStatus.DO_NOT_DISTURB) + .addEventListeners(client, waiter, new Listener(bot)) + .setBulkDeleteSplittingEnabled(true) + .build(); + bot.setJDA(jda); + + // check if something about the current startup is not supported + String unsupportedReason = OtherUtil.getUnsupportedBotReason(jda); + if (unsupportedReason != null) + { + prompt.alert(Prompt.Level.ERROR, "JMusicBot", "JMusicBot cannot be run on this Discord bot: " + unsupportedReason); + try{ Thread.sleep(5000);}catch(InterruptedException ignored){} // this is awful but until we have a better way... + jda.shutdown(); + System.exit(1); + } + + // other check that will just be a warning now but may be required in the future + // check if the user has changed the prefix and provide info about the + // message content intent + if(!"@mention".equals(config.getPrefix())) + { + LOG.info("JMusicBot", "You currently have a custom prefix set. " + + "If your prefix is not working, make sure that the 'MESSAGE CONTENT INTENT' is Enabled " + + "on https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot"); + } + } + catch (LoginException ex) + { + prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nPlease make sure you are " + + "editing the correct config.txt file, and that you have used the " + + "correct token (not the 'secret'!)\nConfig Location: " + config.getConfigLocation()); + System.exit(1); + } + catch(IllegalArgumentException ex) + { + prompt.alert(Prompt.Level.ERROR, "JMusicBot", "Some aspect of the configuration is " + + "invalid: " + ex + "\nConfig Location: " + config.getConfigLocation()); + System.exit(1); + } + catch(ErrorResponseException ex) + { + prompt.alert(Prompt.Level.ERROR, "JMusicBot", ex + "\nInvalid reponse returned when " + + "attempting to connect, please make sure you're connected to the internet"); + System.exit(1); + } + } + + private static CommandClient createCommandClient(BotConfig config, SettingsManager settings, Bot bot) + { + // instantiate about command + AboutCommand aboutCommand = new AboutCommand(Color.BLUE.brighter(), + "a music bot that is [easy to host yourself!](https://github.com/jagrosh/MusicBot) (v" + OtherUtil.getCurrentVersion() + ")", + new String[]{"High-quality music playback", "FairQueue™ Technology", "Easy to host yourself"}, + RECOMMENDED_PERMS); + aboutCommand.setIsAuthor(false); + aboutCommand.setReplacementCharacter("\uD83C\uDFB6"); // 🎶 + + // set up the command client + CommandClientBuilder cb = new CommandClientBuilder() + .setPrefix(config.getPrefix()) + .setAlternativePrefix(config.getAltPrefix()) + .setOwnerId(Long.toString(config.getOwnerId())) + .setEmojis(config.getSuccess(), config.getWarning(), config.getError()) + .setHelpWord(config.getHelp()) + .setLinkedCacheSize(200) + .setGuildSettingsManager(settings) + .addCommands(aboutCommand, + new PingCommand(), + new SettingsCmd(bot), + + new LyricsCmd(bot), + new NowplayingCmd(bot), + new PlayCmd(bot), + new PlaylistsCmd(bot), + new QueueCmd(bot), + new RemoveCmd(bot), + new SearchCmd(bot), + new SCSearchCmd(bot), + new SeekCmd(bot), + new ShuffleCmd(bot), + new SkipCmd(bot), + + new ForceRemoveCmd(bot), + new ForceskipCmd(bot), + new MoveTrackCmd(bot), + new PauseCmd(bot), + new PlaynextCmd(bot), + new RepeatCmd(bot), + new SkiptoCmd(bot), + new StopCmd(bot), + new VolumeCmd(bot), + + new PrefixCmd(bot), + new QueueTypeCmd(bot), + new SetdjCmd(bot), + new SkipratioCmd(bot), + new SettcCmd(bot), + new SetvcCmd(bot), + + new AutoplaylistCmd(bot), + new DebugCmd(bot), + new PlaylistCmd(bot), + new SetavatarCmd(bot), + new SetgameCmd(bot), + new SetnameCmd(bot), + new SetstatusCmd(bot), + new ShutdownCmd(bot) + ); + + // enable eval if applicable + if(config.useEval()) + cb.addCommand(new EvalCmd(bot)); + + // set status if set in config + if(config.getStatus() != OnlineStatus.UNKNOWN) + cb.setStatus(config.getStatus()); + + // set game + if(config.getGame() == null) + cb.useDefaultGame(); + else if(config.isGameNone()) + cb.setActivity(null); + else + cb.setActivity(config.getGame()); + + return cb.build(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/Listener.java b/src/main/java/com/jagrosh/jmusicbot/Listener.java new file mode 100644 index 0000000..f2a456e --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/Listener.java @@ -0,0 +1,107 @@ +package com.jagrosh.jmusicbot; + +import com.jagrosh.jmusicbot.utils.OtherUtil; +import java.util.concurrent.TimeUnit; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.events.ReadyEvent; +import net.dv8tion.jda.api.events.ShutdownEvent; +import net.dv8tion.jda.api.events.guild.GuildJoinEvent; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageDeleteEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Listener extends ListenerAdapter +{ + private final Bot bot; + + public Listener(Bot bot) + { + this.bot = bot; + } + + @Override + public void onReady(ReadyEvent event) + { + if(event.getJDA().getGuildCache().isEmpty()) + { + Logger log = LoggerFactory.getLogger("MusicBot"); + log.warn("This bot is not on any guilds! Use the following link to add the bot to your guilds!"); + log.warn(event.getJDA().getInviteUrl(JMusicBot.RECOMMENDED_PERMS)); + } + credit(event.getJDA()); + event.getJDA().getGuilds().forEach((guild) -> + { + try + { + String defpl = bot.getSettingsManager().getSettings(guild).getDefaultPlaylist(); + VoiceChannel vc = bot.getSettingsManager().getSettings(guild).getVoiceChannel(guild); + if(defpl!=null && vc!=null && bot.getPlayerManager().setUpHandler(guild).playFromDefault()) + { + guild.getAudioManager().openAudioConnection(vc); + } + } + catch(Exception ignore) {} + }); + if(bot.getConfig().useUpdateAlerts()) + { + bot.getThreadpool().scheduleWithFixedDelay(() -> + { + try + { + User owner = bot.getJDA().retrieveUserById(bot.getConfig().getOwnerId()).complete(); + String currentVersion = OtherUtil.getCurrentVersion(); + String latestVersion = OtherUtil.getLatestVersion(); + if(latestVersion!=null && !currentVersion.equalsIgnoreCase(latestVersion)) + { + String msg = String.format(OtherUtil.NEW_VERSION_AVAILABLE, currentVersion, latestVersion); + owner.openPrivateChannel().queue(pc -> pc.sendMessage(msg).queue()); + } + } + catch(Exception ignored) {} // ignored + }, 0, 24, TimeUnit.HOURS); + } + } + + @Override + public void onGuildMessageDelete(GuildMessageDeleteEvent event) + { + bot.getNowplayingHandler().onMessageDelete(event.getGuild(), event.getMessageIdLong()); + } + + @Override + public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) + { + bot.getAloneInVoiceHandler().onVoiceUpdate(event); + } + + @Override + public void onShutdown(ShutdownEvent event) + { + bot.shutdown(); + } + + @Override + public void onGuildJoin(GuildJoinEvent event) + { + credit(event.getJDA()); + } + + // make sure people aren't adding clones to dbots + private void credit(JDA jda) + { + Guild dbots = jda.getGuildById(110373943822540800L); + if(dbots==null) + return; + if(bot.getConfig().getDBots()) + return; + jda.getTextChannelById(119222314964353025L) + .sendMessage("This account is running JMusicBot. Please do not list bot clones on this server, <@"+bot.getConfig().getOwnerId()+">.").complete(); + dbots.leave().queue(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/AloneInVoiceHandler.java b/src/main/java/com/jagrosh/jmusicbot/audio/AloneInVoiceHandler.java new file mode 100644 index 0000000..b64a35b --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/AloneInVoiceHandler.java @@ -0,0 +1,79 @@ +package com.jagrosh.jmusicbot.audio; + +import com.jagrosh.jmusicbot.Bot; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; + +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class AloneInVoiceHandler +{ + private final Bot bot; + private final HashMap aloneSince = new HashMap<>(); + private long aloneTimeUntilStop = 0; + + public AloneInVoiceHandler(Bot bot) + { + this.bot = bot; + } + + public void init() + { + aloneTimeUntilStop = bot.getConfig().getAloneTimeUntilStop(); + if(aloneTimeUntilStop > 0) + bot.getThreadpool().scheduleWithFixedDelay(() -> check(), 0, 5, TimeUnit.SECONDS); + } + + private void check() + { + Set toRemove = new HashSet<>(); + for(Map.Entry entrySet: aloneSince.entrySet()) + { + if(entrySet.getValue().getEpochSecond() > Instant.now().getEpochSecond() - aloneTimeUntilStop) continue; + + Guild guild = bot.getJDA().getGuildById(entrySet.getKey()); + + if(guild == null) + { + toRemove.add(entrySet.getKey()); + continue; + } + + ((AudioHandler) guild.getAudioManager().getSendingHandler()).stopAndClear(); + guild.getAudioManager().closeAudioConnection(); + + toRemove.add(entrySet.getKey()); + } + toRemove.forEach(id -> aloneSince.remove(id)); + } + + public void onVoiceUpdate(GuildVoiceUpdateEvent event) + { + if(aloneTimeUntilStop <= 0) return; + + Guild guild = event.getEntity().getGuild(); + if(!bot.getPlayerManager().hasHandler(guild)) return; + + boolean alone = isAlone(guild); + boolean inList = aloneSince.containsKey(guild.getIdLong()); + + if(!alone && inList) + aloneSince.remove(guild.getIdLong()); + else if(alone && !inList) + aloneSince.put(guild.getIdLong(), Instant.now()); + } + + private boolean isAlone(Guild guild) + { + if(guild.getAudioManager().getConnectedChannel() == null) return false; + return guild.getAudioManager().getConnectedChannel().getMembers().stream() + .noneMatch(x -> + !x.getVoiceState().isDeafened() + && !x.getUser().isBot()); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java b/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java new file mode 100644 index 0000000..8883abd --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/AudioHandler.java @@ -0,0 +1,311 @@ +package com.jagrosh.jmusicbot.audio; + +import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist; +import com.jagrosh.jmusicbot.queue.AbstractQueue; +import com.jagrosh.jmusicbot.settings.QueueType; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.jagrosh.jmusicbot.settings.RepeatMode; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; +import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack; +import java.nio.ByteBuffer; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.audio.AudioSendHandler; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import org.slf4j.LoggerFactory; + +public class AudioHandler extends AudioEventAdapter implements AudioSendHandler +{ + public final static String PLAY_EMOJI = "\u25B6"; // ▶ + public final static String PAUSE_EMOJI = "\u23F8"; // ⏸ + public final static String STOP_EMOJI = "\u23F9"; // ⏹ + + + private final List defaultQueue = new LinkedList<>(); + private final Set votes = new HashSet<>(); + + private final PlayerManager manager; + private final AudioPlayer audioPlayer; + private final long guildId; + + private AudioFrame lastFrame; + private AbstractQueue queue; + + protected AudioHandler(PlayerManager manager, Guild guild, AudioPlayer player) + { + this.manager = manager; + this.audioPlayer = player; + this.guildId = guild.getIdLong(); + + this.setQueueType(manager.getBot().getSettingsManager().getSettings(guildId).getQueueType()); + } + + public void setQueueType(QueueType type) + { + queue = type.createInstance(queue); + } + + public int addTrackToFront(QueuedTrack qtrack) + { + if(audioPlayer.getPlayingTrack()==null) + { + audioPlayer.playTrack(qtrack.getTrack()); + return -1; + } + else + { + queue.addAt(0, qtrack); + return 0; + } + } + + public int addTrack(QueuedTrack qtrack) + { + if(audioPlayer.getPlayingTrack()==null) + { + audioPlayer.playTrack(qtrack.getTrack()); + return -1; + } + else + return queue.add(qtrack); + } + + public AbstractQueue getQueue() + { + return queue; + } + + public void stopAndClear() + { + queue.clear(); + defaultQueue.clear(); + audioPlayer.stopTrack(); + //current = null; + } + + public boolean isMusicPlaying(JDA jda) + { + return guild(jda).getSelfMember().getVoiceState().inVoiceChannel() && audioPlayer.getPlayingTrack()!=null; + } + + public Set getVotes() + { + return votes; + } + + public AudioPlayer getPlayer() + { + return audioPlayer; + } + + public RequestMetadata getRequestMetadata() + { + if(audioPlayer.getPlayingTrack() == null) + return RequestMetadata.EMPTY; + RequestMetadata rm = audioPlayer.getPlayingTrack().getUserData(RequestMetadata.class); + return rm == null ? RequestMetadata.EMPTY : rm; + } + + public boolean playFromDefault() + { + if(!defaultQueue.isEmpty()) + { + audioPlayer.playTrack(defaultQueue.remove(0)); + return true; + } + Settings settings = manager.getBot().getSettingsManager().getSettings(guildId); + if(settings==null || settings.getDefaultPlaylist()==null) + return false; + + Playlist pl = manager.getBot().getPlaylistLoader().getPlaylist(settings.getDefaultPlaylist()); + if(pl==null || pl.getItems().isEmpty()) + return false; + pl.loadTracks(manager, (at) -> + { + if(audioPlayer.getPlayingTrack()==null) + audioPlayer.playTrack(at); + else + defaultQueue.add(at); + }, () -> + { + if(pl.getTracks().isEmpty() && !manager.getBot().getConfig().getStay()) + manager.getBot().closeAudioConnection(guildId); + }); + return true; + } + + // Audio Events + @Override + public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) + { + RepeatMode repeatMode = manager.getBot().getSettingsManager().getSettings(guildId).getRepeatMode(); + // if the track ended normally, and we're in repeat mode, re-add it to the queue + if(endReason==AudioTrackEndReason.FINISHED && repeatMode != RepeatMode.OFF) + { + QueuedTrack clone = new QueuedTrack(track.makeClone(), track.getUserData(RequestMetadata.class)); + if(repeatMode == RepeatMode.ALL) + queue.add(clone); + else + queue.addAt(0, clone); + } + + if(queue.isEmpty()) + { + if(!playFromDefault()) + { + manager.getBot().getNowplayingHandler().onTrackUpdate(null); + if(!manager.getBot().getConfig().getStay()) + manager.getBot().closeAudioConnection(guildId); + // unpause, in the case when the player was paused and the track has been skipped. + // this is to prevent the player being paused next time it's being used. + player.setPaused(false); + } + } + else + { + QueuedTrack qt = queue.pull(); + player.playTrack(qt.getTrack()); + } + } + + @Override + public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) { + LoggerFactory.getLogger("AudioHandler").error("Track " + track.getIdentifier() + " has failed to play", exception); + } + + @Override + public void onTrackStart(AudioPlayer player, AudioTrack track) + { + votes.clear(); + manager.getBot().getNowplayingHandler().onTrackUpdate(track); + } + + + // Formatting + public Message getNowPlaying(JDA jda) + { + if(isMusicPlaying(jda)) + { + Guild guild = guild(jda); + AudioTrack track = audioPlayer.getPlayingTrack(); + MessageBuilder mb = new MessageBuilder(); + mb.append(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing in "+guild.getSelfMember().getVoiceState().getChannel().getAsMention()+"...**")); + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(guild.getSelfMember().getColor()); + RequestMetadata rm = getRequestMetadata(); + if(rm.getOwner() != 0L) + { + User u = guild.getJDA().getUserById(rm.user.id); + if(u==null) + eb.setAuthor(FormatUtil.formatUsername(rm.user), null, rm.user.avatar); + else + eb.setAuthor(FormatUtil.formatUsername(u), null, u.getEffectiveAvatarUrl()); + } + + try + { + eb.setTitle(track.getInfo().title, track.getInfo().uri); + } + catch(Exception e) + { + eb.setTitle(track.getInfo().title); + } + + if(track instanceof YoutubeAudioTrack && manager.getBot().getConfig().useNPImages()) + { + eb.setThumbnail("https://img.youtube.com/vi/"+track.getIdentifier()+"/mqdefault.jpg"); + } + + if(track.getInfo().author != null && !track.getInfo().author.isEmpty()) + eb.setFooter("Source: " + track.getInfo().author, null); + + double progress = (double)audioPlayer.getPlayingTrack().getPosition()/track.getDuration(); + eb.setDescription(getStatusEmoji() + + " "+FormatUtil.progressBar(progress) + + " `[" + TimeUtil.formatTime(track.getPosition()) + "/" + TimeUtil.formatTime(track.getDuration()) + "]` " + + FormatUtil.volumeIcon(audioPlayer.getVolume())); + + return mb.setEmbeds(eb.build()).build(); + } + else return null; + } + + public Message getNoMusicPlaying(JDA jda) + { + Guild guild = guild(jda); + return new MessageBuilder() + .setContent(FormatUtil.filter(manager.getBot().getConfig().getSuccess()+" **Now Playing...**")) + .setEmbeds(new EmbedBuilder() + .setTitle("No music playing") + .setDescription(STOP_EMOJI+" "+FormatUtil.progressBar(-1)+" "+FormatUtil.volumeIcon(audioPlayer.getVolume())) + .setColor(guild.getSelfMember().getColor()) + .build()).build(); + } + + public String getStatusEmoji() + { + return audioPlayer.isPaused() ? PAUSE_EMOJI : PLAY_EMOJI; + } + + // Audio Send Handler methods + /*@Override + public boolean canProvide() + { + if (lastFrame == null) + lastFrame = audioPlayer.provide(); + + return lastFrame != null; + } + + @Override + public byte[] provide20MsAudio() + { + if (lastFrame == null) + lastFrame = audioPlayer.provide(); + + byte[] data = lastFrame != null ? lastFrame.getData() : null; + lastFrame = null; + + return data; + }*/ + + @Override + public boolean canProvide() + { + lastFrame = audioPlayer.provide(); + return lastFrame != null; + } + + @Override + public ByteBuffer provide20MsAudio() + { + return ByteBuffer.wrap(lastFrame.getData()); + } + + @Override + public boolean isOpus() + { + return true; + } + + + // Private methods + private Guild guild(JDA jda) + { + return jda.getGuildById(guildId); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/NowplayingHandler.java b/src/main/java/com/jagrosh/jmusicbot/audio/NowplayingHandler.java new file mode 100644 index 0000000..2a81643 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/NowplayingHandler.java @@ -0,0 +1,104 @@ +package com.jagrosh.jmusicbot.audio; + +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.entities.Pair; +import com.jagrosh.jmusicbot.settings.Settings; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.exceptions.PermissionException; +import net.dv8tion.jda.api.exceptions.RateLimitedException; + +public class NowplayingHandler +{ + private final Bot bot; + private final HashMap> lastNP; // guild -> channel,message + + public NowplayingHandler(Bot bot) + { + this.bot = bot; + this.lastNP = new HashMap<>(); + } + + public void init() + { + if(!bot.getConfig().useNPImages()) + bot.getThreadpool().scheduleWithFixedDelay(() -> updateAll(), 0, 5, TimeUnit.SECONDS); + } + + public void setLastNPMessage(Message m) + { + lastNP.put(m.getGuild().getIdLong(), new Pair<>(m.getTextChannel().getIdLong(), m.getIdLong())); + } + + public void clearLastNPMessage(Guild guild) + { + lastNP.remove(guild.getIdLong()); + } + + private void updateAll() + { + Set toRemove = new HashSet<>(); + for(long guildId: lastNP.keySet()) + { + Guild guild = bot.getJDA().getGuildById(guildId); + if(guild==null) + { + toRemove.add(guildId); + continue; + } + Pair pair = lastNP.get(guildId); + TextChannel tc = guild.getTextChannelById(pair.getKey()); + if(tc==null) + { + toRemove.add(guildId); + continue; + } + AudioHandler handler = (AudioHandler)guild.getAudioManager().getSendingHandler(); + Message msg = handler.getNowPlaying(bot.getJDA()); + if(msg==null) + { + msg = handler.getNoMusicPlaying(bot.getJDA()); + toRemove.add(guildId); + } + try + { + tc.editMessageById(pair.getValue(), msg).queue(m->{}, t -> lastNP.remove(guildId)); + } + catch(Exception e) + { + toRemove.add(guildId); + } + } + toRemove.forEach(id -> lastNP.remove(id)); + } + + // "event"-based methods + public void onTrackUpdate(AudioTrack track) + { + // update bot status if applicable + if(bot.getConfig().getSongInStatus()) + { + if(track!=null && bot.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count()<=1) + bot.getJDA().getPresence().setActivity(Activity.listening(track.getInfo().title)); + else + bot.resetGame(); + } + } + + public void onMessageDelete(Guild guild, long messageId) + { + Pair pair = lastNP.get(guild.getIdLong()); + if(pair==null) + return; + if(pair.getValue() == messageId) + lastNP.remove(guild.getIdLong()); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java b/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java new file mode 100644 index 0000000..0370f3a --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/PlayerManager.java @@ -0,0 +1,102 @@ +package com.jagrosh.jmusicbot.audio; + +import com.dunctebot.sourcemanagers.DuncteBotSources; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.BotConfig; +import com.jagrosh.jmusicbot.utils.YouTubeUtil; +import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.getyarn.GetyarnAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.nico.NicoAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager; +import com.sedmelluq.lava.extensions.youtuberotator.YoutubeIpRotatorSetup; +import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner; +import com.sedmelluq.lava.extensions.youtuberotator.planner.BalancingIpRoutePlanner; +import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block; +import dev.lavalink.youtube.YoutubeAudioSourceManager; +import dev.lavalink.youtube.clients.Web; +import net.dv8tion.jda.api.entities.Guild; + +public class PlayerManager extends DefaultAudioPlayerManager +{ + private final Bot bot; + + private final BotConfig config; + + public PlayerManager(Bot bot, BotConfig config) + { + this.bot = bot; + this.config = config; + } + + public void init() + { + TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t)); + + if (config.getYTPoToken() != null && config.getYTVisitorData() != null) + Web.setPoTokenAndVisitorData(config.getYTPoToken(), config.getYTVisitorData()); + + YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true); + if (config.getYTRoutingPlanner() != YouTubeUtil.RoutingPlanner.NONE) + { + AbstractRoutePlanner routePlanner = YouTubeUtil.createRouterPlanner(config.getYTRoutingPlanner(), config.getYTIpBlocks()); + YoutubeIpRotatorSetup rotator = new YoutubeIpRotatorSetup(routePlanner); + + rotator.forConfiguration(yt.getHttpInterfaceManager(), false) + .withMainDelegateFilter(yt.getContextFilter()) + .setup(); + } + + yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages()); + registerSourceManager(yt); + + registerSourceManager(SoundCloudAudioSourceManager.createDefault()); + registerSourceManager(new BandcampAudioSourceManager()); + registerSourceManager(new VimeoAudioSourceManager()); + registerSourceManager(new TwitchStreamAudioSourceManager()); + registerSourceManager(new BeamAudioSourceManager()); + registerSourceManager(new GetyarnAudioSourceManager()); + registerSourceManager(new NicoAudioSourceManager()); + registerSourceManager(new HttpAudioSourceManager(MediaContainerRegistry.DEFAULT_REGISTRY)); + + AudioSourceManagers.registerLocalSource(this); + + DuncteBotSources.registerAll(this, "en-US"); + } + + public Bot getBot() + { + return bot; + } + + public boolean hasHandler(Guild guild) + { + return guild.getAudioManager().getSendingHandler()!=null; + } + + public AudioHandler setUpHandler(Guild guild) + { + AudioHandler handler; + if(guild.getAudioManager().getSendingHandler()==null) + { + AudioPlayer player = createPlayer(); + player.setVolume(bot.getSettingsManager().getSettings(guild).getVolume()); + handler = new AudioHandler(this, guild, player); + player.addListener(handler); + guild.getAudioManager().setSendingHandler(handler); + } + else + handler = (AudioHandler) guild.getAudioManager().getSendingHandler(); + return handler; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java b/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java new file mode 100644 index 0000000..2c9ed99 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/QueuedTrack.java @@ -0,0 +1,48 @@ +package com.jagrosh.jmusicbot.audio; + +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.jagrosh.jmusicbot.queue.Queueable; +import net.dv8tion.jda.api.entities.User; + +public class QueuedTrack implements Queueable +{ + private final AudioTrack track; + private final RequestMetadata requestMetadata; + + public QueuedTrack(AudioTrack track, RequestMetadata rm) + { + this.track = track; + this.track.setUserData(rm == null ? RequestMetadata.EMPTY : rm); + + this.requestMetadata = rm; + if (this.track.isSeekable() && rm != null) + track.setPosition(rm.requestInfo.startTimestamp); + } + + @Override + public long getIdentifier() + { + return requestMetadata.getOwner(); + } + + public AudioTrack getTrack() + { + return track; + } + + public RequestMetadata getRequestMetadata() + { + return requestMetadata; + } + + @Override + public String toString() + { + String entry = "`[" + TimeUtil.formatTime(track.getDuration()) + "]` "; + AudioTrackInfo trackInfo = track.getInfo(); + entry = entry + (trackInfo.uri.startsWith("http") ? "[**" + trackInfo.title + "**]("+trackInfo.uri+")" : "**" + trackInfo.title + "**"); + return entry + " - <@" + track.getUserData(RequestMetadata.class).getOwner() + ">"; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java b/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java new file mode 100644 index 0000000..0dfb22b --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/RequestMetadata.java @@ -0,0 +1,72 @@ +package com.jagrosh.jmusicbot.audio; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import net.dv8tion.jda.api.entities.User; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RequestMetadata +{ + public static final RequestMetadata EMPTY = new RequestMetadata(null, null); + + public final UserInfo user; + public final RequestInfo requestInfo; + + public RequestMetadata(User user, RequestInfo requestInfo) + { + this.user = user == null ? null : new UserInfo(user.getIdLong(), user.getName(), user.getDiscriminator(), user.getEffectiveAvatarUrl()); + this.requestInfo = requestInfo; + } + + public long getOwner() + { + return user == null ? 0L : user.id; + } + + public static RequestMetadata fromResultHandler(AudioTrack track, CommandEvent event) + { + return new RequestMetadata(event.getAuthor(), new RequestInfo(event.getArgs(), track.getInfo().uri)); + } + + public static class RequestInfo + { + public final String query, url; + public final long startTimestamp; + + public RequestInfo(String query, String url) + { + this(query, url, tryGetTimestamp(query)); + } + + private RequestInfo(String query, String url, long startTimestamp) + { + this.url = url; + this.query = query; + this.startTimestamp = startTimestamp; + } + + private static final Pattern youtubeTimestampPattern = Pattern.compile("youtu(?:\\.be|be\\..+)/.*\\?.*(?!.*list=)t=([\\dhms]+)"); + private static long tryGetTimestamp(String url) + { + Matcher matcher = youtubeTimestampPattern.matcher(url); + return matcher.find() ? TimeUtil.parseUnitTime(matcher.group(1)) : 0; + } + } + + public static class UserInfo + { + public final long id; + public final String username, discrim, avatar; + + private UserInfo(long id, String username, String discrim, String avatar) + { + this.id = id; + this.username = username; + this.discrim = discrim; + this.avatar = avatar; + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java b/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java new file mode 100644 index 0000000..97ff033 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/audio/TransformativeAudioSourceManager.java @@ -0,0 +1,85 @@ +package com.jagrosh.jmusicbot.audio; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import com.sedmelluq.discord.lavaplayer.track.AudioReference; +import com.typesafe.config.Config; +import dev.lavalink.youtube.YoutubeAudioSourceManager; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TransformativeAudioSourceManager extends YoutubeAudioSourceManager +{ + private final static Logger log = LoggerFactory.getLogger(TransformativeAudioSourceManager.class); + private final String name, regex, replacement, selector, format; + + public TransformativeAudioSourceManager(String name, Config object) + { + this(name, object.getString("regex"), object.getString("replacement"), object.getString("selector"), object.getString("format")); + } + + public TransformativeAudioSourceManager(String name, String regex, String replacement, String selector, String format) + { + this.name = name; + this.regex = regex; + this.replacement = replacement; + this.selector = selector; + this.format = format; + } + + @Override + public String getSourceName() + { + return name; + } + + @Override + public AudioItem loadItem(AudioPlayerManager apm, AudioReference ar) + { + if(ar.identifier == null || !ar.identifier.matches(regex)) + return null; + try + { + String url = ar.identifier.replaceAll(regex, replacement); + Document doc = Jsoup.connect(url).get(); + String value = doc.selectFirst(selector).ownText(); + String formattedValue = String.format(format, value); + return super.loadItem(apm, new AudioReference(formattedValue, null)); + } + catch (PatternSyntaxException ex) + { + log.info(String.format("Invalid pattern syntax '%s' in source '%s'", regex, name)); + } + catch (IOException ex) + { + log.warn(String.format("Failed to resolve URL in source '%s': ", name), ex); + } + catch (Exception ex) + { + log.warn(String.format("Exception in source '%s'", name), ex); + } + return null; + } + + public static List createTransforms(Config transforms) + { + try + { + return transforms.root().entrySet().stream() + .map(e -> new TransformativeAudioSourceManager(e.getKey(), transforms.getConfig(e.getKey()))) + .collect(Collectors.toList()); + } + catch (Exception ex) + { + log.warn("Invalid transform ", ex); + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/AdminCommand.java b/src/main/java/com/jagrosh/jmusicbot/commands/AdminCommand.java new file mode 100644 index 0000000..64308fd --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/AdminCommand.java @@ -0,0 +1,20 @@ +package com.jagrosh.jmusicbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import net.dv8tion.jda.api.Permission; + +public abstract class AdminCommand extends Command +{ + public AdminCommand() + { + this.category = new Category("Admin", event -> + { + if(event.getAuthor().getId().equals(event.getClient().getOwnerId())) + return true; + if(event.getGuild()==null) + return true; + return event.getMember().hasPermission(Permission.MANAGE_SERVER); + }); + this.guildOnly = true; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/DJCommand.java b/src/main/java/com/jagrosh/jmusicbot/commands/DJCommand.java new file mode 100644 index 0000000..23628b1 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/DJCommand.java @@ -0,0 +1,29 @@ +package com.jagrosh.jmusicbot.commands; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.settings.Settings; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Role; + +public abstract class DJCommand extends MusicCommand +{ + public DJCommand(Bot bot) + { + super(bot); + this.category = new Category("DJ", event -> checkDJPermission(event)); + } + + public static boolean checkDJPermission(CommandEvent event) + { + if(event.getAuthor().getId().equals(event.getClient().getOwnerId())) + return true; + if(event.getGuild()==null) + return true; + if(event.getMember().hasPermission(Permission.MANAGE_SERVER)) + return true; + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + Role dj = settings.getRole(event.getGuild()); + return dj!=null && (event.getMember().getRoles().contains(dj) || dj.getIdLong()==event.getGuild().getIdLong()); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/MusicCommand.java b/src/main/java/com/jagrosh/jmusicbot/commands/MusicCommand.java new file mode 100644 index 0000000..0b85f65 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/MusicCommand.java @@ -0,0 +1,83 @@ +package com.jagrosh.jmusicbot.commands; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import net.dv8tion.jda.api.entities.GuildVoiceState; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.exceptions.PermissionException; + +public abstract class MusicCommand extends Command +{ + protected final Bot bot; + protected boolean bePlaying; + protected boolean beListening; + + public MusicCommand(Bot bot) + { + this.bot = bot; + this.guildOnly = true; + this.category = new Category("Music"); + } + + @Override + protected void execute(CommandEvent event) + { + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + TextChannel tchannel = settings.getTextChannel(event.getGuild()); + if(tchannel!=null && !event.getTextChannel().equals(tchannel)) + { + try + { + event.getMessage().delete().queue(); + } catch(PermissionException ignore){} + event.replyInDm(event.getClient().getError()+" You can only use that command in "+tchannel.getAsMention()+"!"); + return; + } + bot.getPlayerManager().setUpHandler(event.getGuild()); // no point constantly checking for this later + if(bePlaying && !((AudioHandler)event.getGuild().getAudioManager().getSendingHandler()).isMusicPlaying(event.getJDA())) + { + event.reply(event.getClient().getError()+" There must be music playing to use that!"); + return; + } + if(beListening) + { + VoiceChannel current = event.getGuild().getSelfMember().getVoiceState().getChannel(); + if(current==null) + current = settings.getVoiceChannel(event.getGuild()); + GuildVoiceState userState = event.getMember().getVoiceState(); + if(!userState.inVoiceChannel() || userState.isDeafened() || (current!=null && !userState.getChannel().equals(current))) + { + event.replyError("You must be listening in "+(current==null ? "a voice channel" : current.getAsMention())+" to use that!"); + return; + } + + VoiceChannel afkChannel = userState.getGuild().getAfkChannel(); + if(afkChannel != null && afkChannel.equals(userState.getChannel())) + { + event.replyError("You cannot use that command in an AFK channel!"); + return; + } + + if(!event.getGuild().getSelfMember().getVoiceState().inVoiceChannel()) + { + try + { + event.getGuild().getAudioManager().openAudioConnection(userState.getChannel()); + } + catch(PermissionException ex) + { + event.reply(event.getClient().getError()+" I am unable to connect to "+userState.getChannel().getAsMention()+"!"); + return; + } + } + } + + doCommand(event); + } + + public abstract void doCommand(CommandEvent event); +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/OwnerCommand.java b/src/main/java/com/jagrosh/jmusicbot/commands/OwnerCommand.java new file mode 100644 index 0000000..97e18d6 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/OwnerCommand.java @@ -0,0 +1,12 @@ +package com.jagrosh.jmusicbot.commands; + +import com.jagrosh.jdautilities.command.Command; + +public abstract class OwnerCommand extends Command +{ + public OwnerCommand() + { + this.category = new Category("Owner"); + this.ownerCommand = true; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/PrefixCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/PrefixCmd.java new file mode 100644 index 0000000..ec49082 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/PrefixCmd.java @@ -0,0 +1,39 @@ +package com.jagrosh.jmusicbot.commands.admin; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.Settings; + +public class PrefixCmd extends AdminCommand +{ + public PrefixCmd(Bot bot) + { + this.name = "prefix"; + this.help = "sets a server-specific prefix"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.replyError("Please include a prefix or NONE"); + return; + } + + Settings s = event.getClient().getSettingsFor(event.getGuild()); + if(event.getArgs().equalsIgnoreCase("none")) + { + s.setPrefix(null); + event.replySuccess("Prefix cleared."); + } + else + { + s.setPrefix(event.getArgs()); + event.replySuccess("Custom prefix set to `" + event.getArgs() + "` on *" + event.getGuild().getName() + "*"); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/QueueTypeCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/QueueTypeCmd.java new file mode 100644 index 0000000..5934dfe --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/QueueTypeCmd.java @@ -0,0 +1,56 @@ +package com.jagrosh.jmusicbot.commands.admin; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.QueueType; +import com.jagrosh.jmusicbot.settings.Settings; + +public class QueueTypeCmd extends AdminCommand +{ + public QueueTypeCmd(Bot bot) + { + super(); + this.name = "queuetype"; + this.help = "changes the queue type"; + this.arguments = "[" + String.join("|", QueueType.getNames()) + "]"; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + String args = event.getArgs(); + QueueType value; + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + + if (args.isEmpty()) + { + QueueType currentType = settings.getQueueType(); + event.reply(currentType.getEmoji() + " Current queue type is: `" + currentType.getUserFriendlyName() + "`."); + return; + } + + try + { + value = QueueType.valueOf(args.toUpperCase()); + } + catch (IllegalArgumentException e) + { + event.replyError("Invalid queue type. Valid types are: [" + String.join("|", QueueType.getNames()) + "]"); + return; + } + + if (settings.getQueueType() != value) + { + settings.setQueueType(value); + + AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler(); + if (handler != null) + handler.setQueueType(value); + } + + event.reply(value.getEmoji() + " Queue type was set to `" + value.getUserFriendlyName() + "`."); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetdjCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetdjCmd.java new file mode 100644 index 0000000..e29b985 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetdjCmd.java @@ -0,0 +1,51 @@ +package com.jagrosh.jmusicbot.commands.admin; + +import java.util.List; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.utils.FinderUtil; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.entities.Role; + +public class SetdjCmd extends AdminCommand +{ + public SetdjCmd(Bot bot) + { + this.name = "setdj"; + this.help = "sets the DJ role for certain music commands"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.reply(event.getClient().getError()+" Please include a role name or NONE"); + return; + } + Settings s = event.getClient().getSettingsFor(event.getGuild()); + if(event.getArgs().equalsIgnoreCase("none")) + { + s.setDJRole(null); + event.reply(event.getClient().getSuccess()+" DJ role cleared; Only Admins can use the DJ commands."); + } + else + { + List list = FinderUtil.findRoles(event.getArgs(), event.getGuild()); + if(list.isEmpty()) + event.reply(event.getClient().getWarning()+" No Roles found matching \""+event.getArgs()+"\""); + else if (list.size()>1) + event.reply(event.getClient().getWarning()+FormatUtil.listOfRoles(list, event.getArgs())); + else + { + s.setDJRole(list.get(0)); + event.reply(event.getClient().getSuccess()+" DJ commands can now be used by users with the **"+list.get(0).getName()+"** role."); + } + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/SettcCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SettcCmd.java new file mode 100644 index 0000000..5a0a87b --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SettcCmd.java @@ -0,0 +1,51 @@ +package com.jagrosh.jmusicbot.commands.admin; + +import java.util.List; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.utils.FinderUtil; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.entities.TextChannel; + +public class SettcCmd extends AdminCommand +{ + public SettcCmd(Bot bot) + { + this.name = "settc"; + this.help = "sets the text channel for music commands"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.reply(event.getClient().getError()+" Please include a text channel or NONE"); + return; + } + Settings s = event.getClient().getSettingsFor(event.getGuild()); + if(event.getArgs().equalsIgnoreCase("none")) + { + s.setTextChannel(null); + event.reply(event.getClient().getSuccess()+" Music commands can now be used in any channel"); + } + else + { + List list = FinderUtil.findTextChannels(event.getArgs(), event.getGuild()); + if(list.isEmpty()) + event.reply(event.getClient().getWarning()+" No Text Channels found matching \""+event.getArgs()+"\""); + else if (list.size()>1) + event.reply(event.getClient().getWarning()+FormatUtil.listOfTChannels(list, event.getArgs())); + else + { + s.setTextChannel(list.get(0)); + event.reply(event.getClient().getSuccess()+" Music commands can now only be used in <#"+list.get(0).getId()+">"); + } + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetvcCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetvcCmd.java new file mode 100644 index 0000000..6ded0e8 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SetvcCmd.java @@ -0,0 +1,51 @@ + +package com.jagrosh.jmusicbot.commands.admin; + +import java.util.List; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.utils.FinderUtil; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.entities.VoiceChannel; + +public class SetvcCmd extends AdminCommand +{ + public SetvcCmd(Bot bot) + { + this.name = "setvc"; + this.help = "sets the voice channel for playing music"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.reply(event.getClient().getError()+" Please include a voice channel or NONE"); + return; + } + Settings s = event.getClient().getSettingsFor(event.getGuild()); + if(event.getArgs().equalsIgnoreCase("none")) + { + s.setVoiceChannel(null); + event.reply(event.getClient().getSuccess()+" Music can now be played in any channel"); + } + else + { + List list = FinderUtil.findVoiceChannels(event.getArgs(), event.getGuild()); + if(list.isEmpty()) + event.reply(event.getClient().getWarning()+" No Voice Channels found matching \""+event.getArgs()+"\""); + else if (list.size()>1) + event.reply(event.getClient().getWarning()+FormatUtil.listOfVChannels(list, event.getArgs())); + else + { + s.setVoiceChannel(list.get(0)); + event.reply(event.getClient().getSuccess()+" Music can now only be played in "+list.get(0).getAsMention()); + } + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/admin/SkipratioCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SkipratioCmd.java new file mode 100644 index 0000000..acc2904 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/admin/SkipratioCmd.java @@ -0,0 +1,38 @@ +package com.jagrosh.jmusicbot.commands.admin; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.AdminCommand; +import com.jagrosh.jmusicbot.settings.Settings; + +public class SkipratioCmd extends AdminCommand +{ + public SkipratioCmd(Bot bot) + { + this.name = "setskip"; + this.help = "sets a server-specific skip percentage"; + this.arguments = "<0 - 100>"; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + protected void execute(CommandEvent event) + { + try + { + int val = Integer.parseInt(event.getArgs().endsWith("%") ? event.getArgs().substring(0,event.getArgs().length()-1) : event.getArgs()); + if( val < 0 || val > 100) + { + event.replyError("The provided value must be between 0 and 100!"); + return; + } + Settings s = event.getClient().getSettingsFor(event.getGuild()); + s.setSkipRatio(val / 100.0); + event.replySuccess("Skip percentage has been set to `" + val + "%` of listeners on *" + event.getGuild().getName() + "*"); + } + catch(NumberFormatException ex) + { + event.replyError("Please include an integer between 0 and 100 (default is 55). This number is the percentage of listening users that must vote to skip a song."); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceRemoveCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceRemoveCmd.java new file mode 100644 index 0000000..80274ba --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceRemoveCmd.java @@ -0,0 +1,101 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.utils.FinderUtil; +import com.jagrosh.jdautilities.menu.OrderedMenu; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class ForceRemoveCmd extends DJCommand +{ + public ForceRemoveCmd(Bot bot) + { + super(bot); + this.name = "forceremove"; + this.help = "removes all entries by a user from the queue"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = false; + this.bePlaying = true; + this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS}; + } + + @Override + public void doCommand(CommandEvent event) + { + if (event.getArgs().isEmpty()) + { + event.replyError("You need to mention a user!"); + return; + } + + AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler(); + if (handler.getQueue().isEmpty()) + { + event.replyError("There is nothing in the queue!"); + return; + } + + + User target; + List found = FinderUtil.findMembers(event.getArgs(), event.getGuild()); + + if(found.isEmpty()) + { + event.replyError("Unable to find the user!"); + return; + } + else if(found.size()>1) + { + OrderedMenu.Builder builder = new OrderedMenu.Builder(); + for(int i=0; i removeAllEntries(found.get(i-1).getUser(), event)) + .setText("Found multiple users:") + .setColor(event.getSelfMember().getColor()) + .useNumbers() + .setUsers(event.getAuthor()) + .useCancelButton(true) + .setCancel((msg) -> {}) + .setEventWaiter(bot.getWaiter()) + .setTimeout(1, TimeUnit.MINUTES) + + .build().display(event.getChannel()); + + return; + } + else + { + target = found.get(0).getUser(); + } + + removeAllEntries(target, event); + + } + + private void removeAllEntries(User target, CommandEvent event) + { + int count = ((AudioHandler) event.getGuild().getAudioManager().getSendingHandler()).getQueue().removeAll(target.getIdLong()); + if (count == 0) + { + event.replyWarning("**"+target.getName()+"** doesn't have any songs in the queue!"); + } + else + { + event.replySuccess("Successfully removed `"+count+"` entries from "+FormatUtil.formatUsername(target)+"."); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceskipCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceskipCmd.java new file mode 100644 index 0000000..99928c6 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/ForceskipCmd.java @@ -0,0 +1,30 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.utils.FormatUtil; + +public class ForceskipCmd extends DJCommand +{ + public ForceskipCmd(Bot bot) + { + super(bot); + this.name = "forceskip"; + this.help = "skips the current song"; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + RequestMetadata rm = handler.getRequestMetadata(); + event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title + +"** "+(rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)")); + handler.getPlayer().stopTrack(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/MoveTrackCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/MoveTrackCmd.java new file mode 100644 index 0000000..9199be0 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/MoveTrackCmd.java @@ -0,0 +1,85 @@ +package com.jagrosh.jmusicbot.commands.dj; + + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.queue.AbstractQueue; + +/** + * Command that provides users the ability to move a track in the playlist. + */ +public class MoveTrackCmd extends DJCommand +{ + + public MoveTrackCmd(Bot bot) + { + super(bot); + this.name = "movetrack"; + this.help = "move a track in the current queue to a different position"; + this.arguments = " "; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + int from; + int to; + + String[] parts = event.getArgs().split("\\s+", 2); + if(parts.length < 2) + { + event.replyError("Please include two valid indexes."); + return; + } + + try + { + // Validate the args + from = Integer.parseInt(parts[0]); + to = Integer.parseInt(parts[1]); + } + catch (NumberFormatException e) + { + event.replyError("Please provide two valid indexes."); + return; + } + + if (from == to) + { + event.replyError("Can't move a track to the same position."); + return; + } + + // Validate that from and to are available + AudioHandler handler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler(); + AbstractQueue queue = handler.getQueue(); + if (isUnavailablePosition(queue, from)) + { + String reply = String.format("`%d` is not a valid position in the queue!", from); + event.replyError(reply); + return; + } + if (isUnavailablePosition(queue, to)) + { + String reply = String.format("`%d` is not a valid position in the queue!", to); + event.replyError(reply); + return; + } + + // Move the track + QueuedTrack track = queue.moveItem(from - 1, to - 1); + String trackTitle = track.getTrack().getInfo().title; + String reply = String.format("Moved **%s** from position `%d` to `%d`.", trackTitle, from, to); + event.replySuccess(reply); + } + + private static boolean isUnavailablePosition(AbstractQueue queue, int position) + { + return (position < 1 || position > queue.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/PauseCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PauseCmd.java new file mode 100644 index 0000000..956779e --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PauseCmd.java @@ -0,0 +1,31 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; + +public class PauseCmd extends DJCommand +{ + public PauseCmd(Bot bot) + { + super(bot); + this.name = "pause"; + this.help = "pauses the current song"; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + if(handler.getPlayer().isPaused()) + { + event.replyWarning("The player is already paused! Use `"+event.getClient().getPrefix()+"play` to unpause!"); + return; + } + handler.getPlayer().setPaused(true); + event.replySuccess("Paused **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**. Type `"+event.getClient().getPrefix()+"play` to unpause!"); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java new file mode 100644 index 0000000..2d44c34 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/PlaynextCmd.java @@ -0,0 +1,112 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import net.dv8tion.jda.api.entities.Message; + +public class PlaynextCmd extends DJCommand +{ + private final String loadingEmoji; + + public PlaynextCmd(Bot bot) + { + super(bot); + this.loadingEmoji = bot.getConfig().getLoading(); + this.name = "playnext"; + this.arguments = ""; + this.help = "plays a single song next"; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = false; + } + + @Override + public void doCommand(CommandEvent event) + { + if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty()) + { + event.replyWarning("Please include a song title or URL!"); + return; + } + String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">") + ? event.getArgs().substring(1,event.getArgs().length()-1) + : event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs(); + event.reply(loadingEmoji+" Loading... `["+args+"]`", m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), args, new ResultHandler(m,event,false))); + } + + private class ResultHandler implements AudioLoadResultHandler + { + private final Message m; + private final CommandEvent event; + private final boolean ytsearch; + + private ResultHandler(Message m, CommandEvent event, boolean ytsearch) + { + this.m = m; + this.event = event; + this.ytsearch = ytsearch; + } + + private void loadSingle(AudioTrack track) + { + if(bot.getConfig().isTooLong(track)) + { + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + return; + } + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + int pos = handler.addTrackToFront(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; + String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title + +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); + m.editMessage(addMsg).queue(); + } + + @Override + public void trackLoaded(AudioTrack track) + { + loadSingle(track); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) + { + AudioTrack single; + if(playlist.getTracks().size()==1 || playlist.isSearchResult()) + single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack(); + else if (playlist.getSelectedTrack()!=null) + single = playlist.getSelectedTrack(); + else + single = playlist.getTracks().get(0); + loadSingle(single); + } + + @Override + public void noMatches() + { + if(ytsearch) + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" No results found for `"+event.getArgs()+"`.")).queue(); + else + bot.getPlayerManager().loadItemOrdered(event.getGuild(), "ytsearch:"+event.getArgs(), new ResultHandler(m,event,true)); + } + + @Override + public void loadFailed(FriendlyException throwable) + { + if(throwable.severity==FriendlyException.Severity.COMMON) + m.editMessage(event.getClient().getError()+" Error loading: "+throwable.getMessage()).queue(); + else + m.editMessage(event.getClient().getError()+" Error loading track.").queue(); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/RepeatCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/RepeatCmd.java new file mode 100644 index 0000000..235cc85 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/RepeatCmd.java @@ -0,0 +1,58 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.settings.RepeatMode; +import com.jagrosh.jmusicbot.settings.Settings; + +public class RepeatCmd extends DJCommand +{ + public RepeatCmd(Bot bot) + { + super(bot); + this.name = "repeat"; + this.help = "re-adds music to the queue when finished"; + this.arguments = "[off|all|single]"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = true; + } + + // override musiccommand's execute because we don't actually care where this is used + @Override + protected void execute(CommandEvent event) + { + String args = event.getArgs(); + RepeatMode value; + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + if(args.isEmpty()) + { + if(settings.getRepeatMode() == RepeatMode.OFF) + value = RepeatMode.ALL; + else + value = RepeatMode.OFF; + } + else if(args.equalsIgnoreCase("false") || args.equalsIgnoreCase("off")) + { + value = RepeatMode.OFF; + } + else if(args.equalsIgnoreCase("true") || args.equalsIgnoreCase("on") || args.equalsIgnoreCase("all")) + { + value = RepeatMode.ALL; + } + else if(args.equalsIgnoreCase("one") || args.equalsIgnoreCase("single")) + { + value = RepeatMode.SINGLE; + } + else + { + event.replyError("Valid options are `off`, `all` or `single` (or leave empty to toggle between `off` and `all`)"); + return; + } + settings.setRepeatMode(value); + event.replySuccess("Repeat mode is now `"+value.getUserFriendlyName()+"`"); + } + + @Override + public void doCommand(CommandEvent event) { /* Intentionally Empty */ } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/SkiptoCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/SkiptoCmd.java new file mode 100644 index 0000000..45c6841 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/SkiptoCmd.java @@ -0,0 +1,43 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; + +public class SkiptoCmd extends DJCommand +{ + public SkiptoCmd(Bot bot) + { + super(bot); + this.name = "skipto"; + this.help = "skips to the specified song"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + int index = 0; + try + { + index = Integer.parseInt(event.getArgs()); + } + catch(NumberFormatException e) + { + event.reply(event.getClient().getError()+" `"+event.getArgs()+"` is not a valid integer!"); + return; + } + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + if(index<1 || index>handler.getQueue().size()) + { + event.reply(event.getClient().getError()+" Position must be a valid integer between 1 and "+handler.getQueue().size()+"!"); + return; + } + handler.getQueue().skip(index-1); + event.reply(event.getClient().getSuccess()+" Skipped to **"+handler.getQueue().get(0).getTrack().getInfo().title+"**"); + handler.getPlayer().stopTrack(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/StopCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/StopCmd.java new file mode 100644 index 0000000..e1ec50d --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/StopCmd.java @@ -0,0 +1,27 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; + +public class StopCmd extends DJCommand +{ + public StopCmd(Bot bot) + { + super(bot); + this.name = "stop"; + this.help = "stops the current song and clears the queue"; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = false; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + handler.stopAndClear(); + event.getGuild().getAudioManager().closeAudioConnection(); + event.reply(event.getClient().getSuccess()+" The player has stopped and the queue has been cleared."); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/dj/VolumeCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/dj/VolumeCmd.java new file mode 100644 index 0000000..43bc5ca --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/dj/VolumeCmd.java @@ -0,0 +1,50 @@ +package com.jagrosh.jmusicbot.commands.dj; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; + +public class VolumeCmd extends DJCommand +{ + public VolumeCmd(Bot bot) + { + super(bot); + this.name = "volume"; + this.aliases = bot.getConfig().getAliases(this.name); + this.help = "sets or shows volume"; + this.arguments = "[0-150]"; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + int volume = handler.getPlayer().getVolume(); + if(event.getArgs().isEmpty()) + { + event.reply(FormatUtil.volumeIcon(volume)+" Current volume is `"+volume+"`"); + } + else + { + int nvolume; + try{ + nvolume = Integer.parseInt(event.getArgs()); + }catch(NumberFormatException e){ + nvolume = -1; + } + if(nvolume<0 || nvolume>150) + event.reply(event.getClient().getError()+" Volume must be a valid integer between 0 and 150!"); + else + { + handler.getPlayer().setVolume(nvolume); + settings.setVolume(nvolume); + event.reply(FormatUtil.volumeIcon(nvolume)+" Volume changed from `"+volume+"` to `"+nvolume+"`"); + } + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/general/SettingsCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/general/SettingsCmd.java new file mode 100644 index 0000000..1eebb3e --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/general/SettingsCmd.java @@ -0,0 +1,59 @@ +package com.jagrosh.jmusicbot.commands.general; + +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.settings.QueueType; +import com.jagrosh.jmusicbot.settings.RepeatMode; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.VoiceChannel; + +public class SettingsCmd extends Command +{ + private final static String EMOJI = "\uD83C\uDFA7"; // 🎧 + + public SettingsCmd(Bot bot) + { + this.name = "settings"; + this.help = "shows the bots settings"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = true; + } + + @Override + protected void execute(CommandEvent event) + { + Settings s = event.getClient().getSettingsFor(event.getGuild()); + MessageBuilder builder = new MessageBuilder() + .append(EMOJI + " **") + .append(FormatUtil.filter(event.getSelfUser().getName())) + .append("** settings:"); + TextChannel tchan = s.getTextChannel(event.getGuild()); + VoiceChannel vchan = s.getVoiceChannel(event.getGuild()); + Role role = s.getRole(event.getGuild()); + EmbedBuilder ebuilder = new EmbedBuilder() + .setColor(event.getSelfMember().getColor()) + .setDescription("Text Channel: " + (tchan == null ? "Any" : "**#" + tchan.getName() + "**") + + "\nVoice Channel: " + (vchan == null ? "Any" : vchan.getAsMention()) + + "\nDJ Role: " + (role == null ? "None" : "**" + role.getName() + "**") + + "\nCustom Prefix: " + (s.getPrefix() == null ? "None" : "`" + s.getPrefix() + "`") + + "\nRepeat Mode: " + (s.getRepeatMode() == RepeatMode.OFF + ? s.getRepeatMode().getUserFriendlyName() + : "**"+s.getRepeatMode().getUserFriendlyName()+"**") + + "\nQueue Type: " + (s.getQueueType() == QueueType.FAIR + ? s.getQueueType().getUserFriendlyName() + : "**"+s.getQueueType().getUserFriendlyName()+"**") + + "\nDefault Playlist: " + (s.getDefaultPlaylist() == null ? "None" : "**" + s.getDefaultPlaylist() + "**") + ) + .setFooter(event.getJDA().getGuilds().size() + " servers | " + + event.getJDA().getGuilds().stream().filter(g -> g.getSelfMember().getVoiceState().inVoiceChannel()).count() + + " audio connections", null); + event.getChannel().sendMessage(builder.setEmbeds(ebuilder.build()).build()).queue(); + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/LyricsCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/LyricsCmd.java new file mode 100644 index 0000000..ad68163 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/LyricsCmd.java @@ -0,0 +1,81 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jlyrics.LyricsClient; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.Permission; + +public class LyricsCmd extends MusicCommand +{ + private final LyricsClient client = new LyricsClient(); + + public LyricsCmd(Bot bot) + { + super(bot); + this.name = "lyrics"; + this.arguments = "[song name]"; + this.help = "shows the lyrics of a song"; + this.aliases = bot.getConfig().getAliases(this.name); + this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS}; + } + + @Override + public void doCommand(CommandEvent event) + { + String title; + if(event.getArgs().isEmpty()) + { + AudioHandler sendingHandler = (AudioHandler) event.getGuild().getAudioManager().getSendingHandler(); + if (sendingHandler.isMusicPlaying(event.getJDA())) + title = sendingHandler.getPlayer().getPlayingTrack().getInfo().title; + else + { + event.replyError("There must be music playing to use that!"); + return; + } + } + else + title = event.getArgs(); + event.getChannel().sendTyping().queue(); + client.getLyrics(title).thenAccept(lyrics -> + { + if(lyrics == null) + { + event.replyError("Lyrics for `" + title + "` could not be found!" + (event.getArgs().isEmpty() ? " Try entering the song name manually (`lyrics [song name]`)" : "")); + return; + } + + EmbedBuilder eb = new EmbedBuilder() + .setAuthor(lyrics.getAuthor()) + .setColor(event.getSelfMember().getColor()) + .setTitle(lyrics.getTitle(), lyrics.getURL()); + if(lyrics.getContent().length()>15000) + { + event.replyWarning("Lyrics for `" + title + "` found but likely not correct: " + lyrics.getURL()); + } + else if(lyrics.getContent().length()>2000) + { + String content = lyrics.getContent().trim(); + while(content.length() > 2000) + { + int index = content.lastIndexOf("\n\n", 2000); + if(index == -1) + index = content.lastIndexOf("\n", 2000); + if(index == -1) + index = content.lastIndexOf(" ", 2000); + if(index == -1) + index = 2000; + event.reply(eb.setDescription(content.substring(0, index).trim()).build()); + content = content.substring(index).trim(); + eb.setAuthor(null).setTitle(null, null); + } + event.reply(eb.setDescription(content).build()); + } + else + event.reply(eb.setDescription(lyrics.getContent()).build()); + }); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/NowplayingCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/NowplayingCmd.java new file mode 100644 index 0000000..3719d9f --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/NowplayingCmd.java @@ -0,0 +1,36 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; + +public class NowplayingCmd extends MusicCommand +{ + public NowplayingCmd(Bot bot) + { + super(bot); + this.name = "nowplaying"; + this.help = "shows the song that is currently playing"; + this.aliases = bot.getConfig().getAliases(this.name); + this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS}; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + Message m = handler.getNowPlaying(event.getJDA()); + if(m==null) + { + event.reply(handler.getNoMusicPlaying(event.getJDA())); + bot.getNowplayingHandler().clearLastNPMessage(event.getGuild()); + } + else + { + event.reply(m, msg -> bot.getNowplayingHandler().setLastNPMessage(msg)); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java new file mode 100644 index 0000000..7a4335f --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlayCmd.java @@ -0,0 +1,244 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.menu.ButtonMenu; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.commands.DJCommand; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import java.util.concurrent.TimeUnit; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.exceptions.PermissionException; + +public class PlayCmd extends MusicCommand +{ + private final static String LOAD = "\uD83D\uDCE5"; // 📥 + private final static String CANCEL = "\uD83D\uDEAB"; // 🚫 + + private final String loadingEmoji; + + public PlayCmd(Bot bot) + { + super(bot); + this.loadingEmoji = bot.getConfig().getLoading(); + this.name = "play"; + this.arguments = ""; + this.help = "plays the provided song"; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = false; + this.children = new Command[]{new PlaylistCmd(bot)}; + } + + @Override + public void doCommand(CommandEvent event) + { + if(event.getArgs().isEmpty() && event.getMessage().getAttachments().isEmpty()) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + if(handler.getPlayer().getPlayingTrack()!=null && handler.getPlayer().isPaused()) + { + if(DJCommand.checkDJPermission(event)) + { + handler.getPlayer().setPaused(false); + event.replySuccess("Resumed **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**."); + } + else + event.replyError("Only DJs can unpause the player!"); + return; + } + StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Play Commands:\n"); + builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ` - plays the first result from Youtube"); + builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ` - plays the provided song, playlist, or stream"); + for(Command cmd: children) + builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName()).append(" ").append(cmd.getArguments()).append("` - ").append(cmd.getHelp()); + event.reply(builder.toString()); + return; + } + String args = event.getArgs().startsWith("<") && event.getArgs().endsWith(">") + ? event.getArgs().substring(1,event.getArgs().length()-1) + : event.getArgs().isEmpty() ? event.getMessage().getAttachments().get(0).getUrl() : event.getArgs(); + event.reply(loadingEmoji+" Loading... `["+args+"]`", m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), args, new ResultHandler(m,event,false))); + } + + private class ResultHandler implements AudioLoadResultHandler + { + private final Message m; + private final CommandEvent event; + private final boolean ytsearch; + + private ResultHandler(Message m, CommandEvent event, boolean ytsearch) + { + this.m = m; + this.event = event; + this.ytsearch = ytsearch; + } + + private void loadSingle(AudioTrack track, AudioPlaylist playlist) + { + if(bot.getConfig().isTooLong(track)) + { + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + + TimeUtil.formatTime(track.getDuration())+"` > `"+ TimeUtil.formatTime(bot.getConfig().getMaxSeconds()*1000)+"`")).queue(); + return; + } + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; + String addMsg = FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title + +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0?"to begin playing":" to the queue at position "+pos)); + if(playlist==null || !event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ADD_REACTION)) + m.editMessage(addMsg).queue(); + else + { + new ButtonMenu.Builder() + .setText(addMsg+"\n"+event.getClient().getWarning()+" This track has a playlist of **"+playlist.getTracks().size()+"** tracks attached. Select "+LOAD+" to load playlist.") + .setChoices(LOAD, CANCEL) + .setEventWaiter(bot.getWaiter()) + .setTimeout(30, TimeUnit.SECONDS) + .setAction(re -> + { + if(re.getName().equals(LOAD)) + m.editMessage(addMsg+"\n"+event.getClient().getSuccess()+" Loaded **"+loadPlaylist(playlist, track)+"** additional tracks!").queue(); + else + m.editMessage(addMsg).queue(); + }).setFinalAction(m -> + { + try{ m.clearReactions().queue(); }catch(PermissionException ignore) {} + }).build().display(m); + } + } + + private int loadPlaylist(AudioPlaylist playlist, AudioTrack exclude) + { + int[] count = {0}; + playlist.getTracks().stream().forEach((track) -> { + if(!bot.getConfig().isTooLong(track) && !track.equals(exclude)) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event))); + count[0]++; + } + }); + return count[0]; + } + + @Override + public void trackLoaded(AudioTrack track) + { + loadSingle(track, null); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) + { + if(playlist.getTracks().size()==1 || playlist.isSearchResult()) + { + AudioTrack single = playlist.getSelectedTrack()==null ? playlist.getTracks().get(0) : playlist.getSelectedTrack(); + loadSingle(single, null); + } + else if (playlist.getSelectedTrack()!=null) + { + AudioTrack single = playlist.getSelectedTrack(); + loadSingle(single, playlist); + } + else + { + int count = loadPlaylist(playlist, null); + if(playlist.getTracks().size() == 0) + { + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" The playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName() + +"**) ")+" could not be loaded or contained 0 entries")).queue(); + } + else if(count==0) + { + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" All entries in this playlist "+(playlist.getName()==null ? "" : "(**"+playlist.getName() + +"**) ")+"were longer than the allowed maximum (`"+bot.getConfig().getMaxTime()+"`)")).queue(); + } + else + { + m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Found " + +(playlist.getName()==null?"a playlist":"playlist **"+playlist.getName()+"**")+" with `" + + playlist.getTracks().size()+"` entries; added to the queue!" + + (count + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + playlist.loadTracks(bot.getPlayerManager(), (at)->handler.addTrack(new QueuedTrack(at, RequestMetadata.fromResultHandler(at, event))), () -> { + StringBuilder builder = new StringBuilder(playlist.getTracks().isEmpty() + ? event.getClient().getWarning()+" No tracks were loaded!" + : event.getClient().getSuccess()+" Loaded **"+playlist.getTracks().size()+"** tracks!"); + if(!playlist.getErrors().isEmpty()) + builder.append("\nThe following tracks failed to load:"); + playlist.getErrors().forEach(err -> builder.append("\n`[").append(err.getIndex()+1).append("]` **").append(err.getItem()).append("**: ").append(err.getReason())); + String str = builder.toString(); + if(str.length()>2000) + str = str.substring(0,1994)+" (...)"; + m.editMessage(FormatUtil.filter(str)).queue(); + }); + }); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/PlaylistsCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlaylistsCmd.java new file mode 100644 index 0000000..e9ae879 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/PlaylistsCmd.java @@ -0,0 +1,44 @@ +package com.jagrosh.jmusicbot.commands.music; + +import java.util.List; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.MusicCommand; + +public class PlaylistsCmd extends MusicCommand +{ + public PlaylistsCmd(Bot bot) + { + super(bot); + this.name = "playlists"; + this.help = "shows the available playlists"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = true; + this.beListening = false; + this.beListening = false; + } + + @Override + public void doCommand(CommandEvent event) + { + if(!bot.getPlaylistLoader().folderExists()) + bot.getPlaylistLoader().createFolder(); + if(!bot.getPlaylistLoader().folderExists()) + { + event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!"); + return; + } + List list = bot.getPlaylistLoader().getPlaylistNames(); + if(list==null) + event.reply(event.getClient().getError()+" Failed to load available playlists!"); + else if(list.isEmpty()) + event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!"); + else + { + StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n"); + list.forEach(str -> builder.append("`").append(str).append("` ")); + builder.append("\nType `").append(event.getClient().getTextualPrefix()).append("play playlist ` to play a playlist"); + event.reply(builder.toString()); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java new file mode 100644 index 0000000..c0cec9f --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/QueueCmd.java @@ -0,0 +1,101 @@ +package com.jagrosh.jmusicbot.commands.music; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.menu.Paginator; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.settings.QueueType; +import com.jagrosh.jmusicbot.settings.RepeatMode; +import com.jagrosh.jmusicbot.settings.Settings; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.exceptions.PermissionException; + +public class QueueCmd extends MusicCommand +{ + private final Paginator.Builder builder; + + public QueueCmd(Bot bot) + { + super(bot); + this.name = "queue"; + this.help = "shows the current queue"; + this.arguments = "[pagenum]"; + this.aliases = bot.getConfig().getAliases(this.name); + this.bePlaying = true; + this.botPermissions = new Permission[]{Permission.MESSAGE_ADD_REACTION,Permission.MESSAGE_EMBED_LINKS}; + builder = new Paginator.Builder() + .setColumns(1) + .setFinalAction(m -> {try{m.clearReactions().queue();}catch(PermissionException ignore){}}) + .setItemsPerPage(10) + .waitOnSinglePage(false) + .useNumberedItems(true) + .showPageNumbers(true) + .wrapPageEnds(true) + .setEventWaiter(bot.getWaiter()) + .setTimeout(1, TimeUnit.MINUTES); + } + + @Override + public void doCommand(CommandEvent event) + { + int pagenum = 1; + try + { + pagenum = Integer.parseInt(event.getArgs()); + } + catch(NumberFormatException ignore){} + AudioHandler ah = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + List list = ah.getQueue().getList(); + if(list.isEmpty()) + { + Message nowp = ah.getNowPlaying(event.getJDA()); + Message nonowp = ah.getNoMusicPlaying(event.getJDA()); + Message built = new MessageBuilder() + .setContent(event.getClient().getWarning() + " There is no music in the queue!") + .setEmbeds((nowp==null ? nonowp : nowp).getEmbeds().get(0)).build(); + event.reply(built, m -> + { + if(nowp!=null) + bot.getNowplayingHandler().setLastNPMessage(m); + }); + return; + } + String[] songs = new String[list.size()]; + long total = 0; + for(int i=0; i getQueueTitle(ah, event.getClient().getSuccess(), songs.length, fintotal, settings.getRepeatMode(), settings.getQueueType())) + .setItems(songs) + .setUsers(event.getAuthor()) + .setColor(event.getSelfMember().getColor()) + ; + builder.build().paginate(event.getChannel(), pagenum); + } + + private String getQueueTitle(AudioHandler ah, String success, int songslength, long total, RepeatMode repeatmode, QueueType queueType) + { + StringBuilder sb = new StringBuilder(); + if(ah.getPlayer().getPlayingTrack()!=null) + { + sb.append(ah.getStatusEmoji()).append(" **") + .append(ah.getPlayer().getPlayingTrack().getInfo().title).append("**\n"); + } + return FormatUtil.filter(sb.append(success).append(" Current Queue | ").append(songslength) + .append(" entries | `").append(TimeUtil.formatTime(total)).append("` ") + .append("| ").append(queueType.getEmoji()).append(" `").append(queueType.getUserFriendlyName()).append('`') + .append(repeatmode.getEmoji() != null ? " | "+repeatmode.getEmoji() : "").toString()); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/RemoveCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/RemoveCmd.java new file mode 100644 index 0000000..e36338b --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/RemoveCmd.java @@ -0,0 +1,81 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.settings.Settings; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.User; + +public class RemoveCmd extends MusicCommand +{ + public RemoveCmd(Bot bot) + { + super(bot); + this.name = "remove"; + this.help = "removes a song from the queue"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + if(handler.getQueue().isEmpty()) + { + event.replyError("There is nothing in the queue!"); + return; + } + if(event.getArgs().equalsIgnoreCase("all")) + { + int count = handler.getQueue().removeAll(event.getAuthor().getIdLong()); + if(count==0) + event.replyWarning("You don't have any songs in the queue!"); + else + event.replySuccess("Successfully removed your "+count+" entries."); + return; + } + int pos; + try { + pos = Integer.parseInt(event.getArgs()); + } catch(NumberFormatException e) { + pos = 0; + } + if(pos<1 || pos>handler.getQueue().size()) + { + event.replyError("Position must be a valid integer between 1 and "+handler.getQueue().size()+"!"); + return; + } + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + boolean isDJ = event.getMember().hasPermission(Permission.MANAGE_SERVER); + if(!isDJ) + isDJ = event.getMember().getRoles().contains(settings.getRole(event.getGuild())); + QueuedTrack qt = handler.getQueue().get(pos-1); + if(qt.getIdentifier()==event.getAuthor().getIdLong()) + { + handler.getQueue().remove(pos-1); + event.replySuccess("Removed **"+qt.getTrack().getInfo().title+"** from the queue"); + } + else if(isDJ) + { + handler.getQueue().remove(pos-1); + User u; + try { + u = event.getJDA().getUserById(qt.getIdentifier()); + } catch(Exception e) { + u = null; + } + event.replySuccess("Removed **"+qt.getTrack().getInfo().title + +"** from the queue (requested by "+(u==null ? "someone" : "**"+u.getName()+"**")+")"); + } + else + { + event.replyError("You cannot remove **"+qt.getTrack().getInfo().title+"** because you didn't add it!"); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/SCSearchCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/SCSearchCmd.java new file mode 100644 index 0000000..99a925d --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/SCSearchCmd.java @@ -0,0 +1,15 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jmusicbot.Bot; + +public class SCSearchCmd extends SearchCmd +{ + public SCSearchCmd(Bot bot) + { + super(bot); + this.searchPrefix = "scsearch:"; + this.name = "scsearch"; + this.help = "searches Soundcloud for a provided query"; + this.aliases = bot.getConfig().getAliases(this.name); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java new file mode 100644 index 0000000..0ac6730 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/SearchCmd.java @@ -0,0 +1,131 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.utils.TimeUtil; +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import java.util.concurrent.TimeUnit; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.menu.OrderedMenu; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.QueuedTrack; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.utils.FormatUtil; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; + +public class SearchCmd extends MusicCommand +{ + protected String searchPrefix = "ytsearch:"; + private final OrderedMenu.Builder builder; + private final String searchingEmoji; + + public SearchCmd(Bot bot) + { + super(bot); + this.searchingEmoji = bot.getConfig().getSearching(); + this.name = "search"; + this.aliases = bot.getConfig().getAliases(this.name); + this.arguments = ""; + this.help = "searches Youtube for a provided query"; + this.beListening = true; + this.bePlaying = false; + this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS}; + builder = new OrderedMenu.Builder() + .allowTextInput(true) + .useNumbers() + .useCancelButton(true) + .setEventWaiter(bot.getWaiter()) + .setTimeout(1, TimeUnit.MINUTES); + } + @Override + public void doCommand(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.replyError("Please include a query."); + return; + } + event.reply(searchingEmoji+" Searching... `["+event.getArgs()+"]`", + m -> bot.getPlayerManager().loadItemOrdered(event.getGuild(), searchPrefix + event.getArgs(), new ResultHandler(m,event))); + } + + private class ResultHandler implements AudioLoadResultHandler + { + private final Message m; + private final CommandEvent event; + + private ResultHandler(Message m, CommandEvent event) + { + this.m = m; + this.event = event; + } + + @Override + public void trackLoaded(AudioTrack track) + { + if(bot.getConfig().isTooLong(track)) + { + m.editMessage(FormatUtil.filter(event.getClient().getWarning()+" This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + + TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`")).queue(); + return; + } + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; + m.editMessage(FormatUtil.filter(event.getClient().getSuccess()+" Added **"+track.getInfo().title + +"** (`"+ TimeUtil.formatTime(track.getDuration())+"`) "+(pos==0 ? "to begin playing" + : " to the queue at position "+pos))).queue(); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) + { + builder.setColor(event.getSelfMember().getColor()) + .setText(FormatUtil.filter(event.getClient().getSuccess()+" Search results for `"+event.getArgs()+"`:")) + .setChoices(new String[0]) + .setSelection((msg,i) -> + { + AudioTrack track = playlist.getTracks().get(i-1); + if(bot.getConfig().isTooLong(track)) + { + event.replyWarning("This track (**"+track.getInfo().title+"**) is longer than the allowed maximum: `" + + TimeUtil.formatTime(track.getDuration())+"` > `"+bot.getConfig().getMaxTime()+"`"); + return; + } + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + int pos = handler.addTrack(new QueuedTrack(track, RequestMetadata.fromResultHandler(track, event)))+1; + event.replySuccess("Added **" + FormatUtil.filter(track.getInfo().title) + + "** (`" + TimeUtil.formatTime(track.getDuration()) + "`) " + (pos==0 ? "to begin playing" + : " to the queue at position "+pos)); + }) + .setCancel((msg) -> {}) + .setUsers(event.getAuthor()) + ; + for(int i=0; i<4 && i trackDuration) + { + event.replyError("Cannot seek to `" + TimeUtil.formatTime(seekMilliseconds) + "` because the current track is `" + TimeUtil.formatTime(trackDuration) + "` long!"); + return; + } + + try + { + playingTrack.setPosition(seekMilliseconds); + } + catch (Exception e) + { + event.replyError("An error occurred while trying to seek: " + e.getMessage()); + LOG.warn("Failed to seek track " + playingTrack.getIdentifier(), e); + return; + } + event.replySuccess("Successfully seeked to `" + TimeUtil.formatTime(playingTrack.getPosition()) + "/" + TimeUtil.formatTime(playingTrack.getDuration()) + "`!"); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/ShuffleCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/ShuffleCmd.java new file mode 100644 index 0000000..c5cec40 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/ShuffleCmd.java @@ -0,0 +1,39 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.commands.MusicCommand; + +public class ShuffleCmd extends MusicCommand +{ + public ShuffleCmd(Bot bot) + { + super(bot); + this.name = "shuffle"; + this.help = "shuffles songs you have added"; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + int s = handler.getQueue().shuffle(event.getAuthor().getIdLong()); + switch (s) + { + case 0: + event.replyError("You don't have any music in the queue to shuffle!"); + break; + case 1: + event.replyWarning("You only have one song in the queue!"); + break; + default: + event.replySuccess("You successfully shuffled your "+s+" entries."); + break; + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/music/SkipCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/music/SkipCmd.java new file mode 100644 index 0000000..1d429fc --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/music/SkipCmd.java @@ -0,0 +1,62 @@ +package com.jagrosh.jmusicbot.commands.music; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.audio.AudioHandler; +import com.jagrosh.jmusicbot.audio.RequestMetadata; +import com.jagrosh.jmusicbot.commands.MusicCommand; +import com.jagrosh.jmusicbot.utils.FormatUtil; + +public class SkipCmd extends MusicCommand +{ + public SkipCmd(Bot bot) + { + super(bot); + this.name = "skip"; + this.help = "votes to skip the current song"; + this.aliases = bot.getConfig().getAliases(this.name); + this.beListening = true; + this.bePlaying = true; + } + + @Override + public void doCommand(CommandEvent event) + { + AudioHandler handler = (AudioHandler)event.getGuild().getAudioManager().getSendingHandler(); + RequestMetadata rm = handler.getRequestMetadata(); + double skipRatio = bot.getSettingsManager().getSettings(event.getGuild()).getSkipRatio(); + if(skipRatio == -1) { + skipRatio = bot.getConfig().getSkipRatio(); + } + if(event.getAuthor().getIdLong() == rm.getOwner() || skipRatio == 0) + { + event.reply(event.getClient().getSuccess()+" Skipped **"+handler.getPlayer().getPlayingTrack().getInfo().title+"**"); + handler.getPlayer().stopTrack(); + } + else + { + int listeners = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream() + .filter(m -> !m.getUser().isBot() && !m.getVoiceState().isDeafened()).count(); + String msg; + if(handler.getVotes().contains(event.getAuthor().getId())) + msg = event.getClient().getWarning()+" You already voted to skip this song `["; + else + { + msg = event.getClient().getSuccess()+" You voted to skip the song `["; + handler.getVotes().add(event.getAuthor().getId()); + } + int skippers = (int)event.getSelfMember().getVoiceState().getChannel().getMembers().stream() + .filter(m -> handler.getVotes().contains(m.getUser().getId())).count(); + int required = (int)Math.ceil(listeners * skipRatio); + msg += skippers + " votes, " + required + "/" + listeners + " needed]`"; + if(skippers>=required) + { + msg += "\n" + event.getClient().getSuccess() + " Skipped **" + handler.getPlayer().getPlayingTrack().getInfo().title + + "** " + (rm.getOwner() == 0L ? "(autoplay)" : "(requested by **" + FormatUtil.formatUsername(rm.user) + "**)"); + handler.getPlayer().stopTrack(); + } + event.reply(msg); + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/AutoplaylistCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/AutoplaylistCmd.java new file mode 100644 index 0000000..da58d0f --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/AutoplaylistCmd.java @@ -0,0 +1,49 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import com.jagrosh.jmusicbot.settings.Settings; + +public class AutoplaylistCmd extends OwnerCommand +{ + private final Bot bot; + + public AutoplaylistCmd(Bot bot) + { + this.bot = bot; + this.guildOnly = true; + this.name = "autoplaylist"; + this.arguments = ""; + this.help = "sets the default playlist for the server"; + this.aliases = bot.getConfig().getAliases(this.name); + } + + @Override + public void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.reply(event.getClient().getError()+" Please include a playlist name or NONE"); + return; + } + if(event.getArgs().equalsIgnoreCase("none")) + { + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + settings.setDefaultPlaylist(null); + event.reply(event.getClient().getSuccess()+" Cleared the default playlist for **"+event.getGuild().getName()+"**"); + return; + } + String pname = event.getArgs().replaceAll("\\s+", "_"); + if(bot.getPlaylistLoader().getPlaylist(pname)==null) + { + event.reply(event.getClient().getError()+" Could not find `"+pname+".txt`!"); + } + else + { + Settings settings = event.getClient().getSettingsFor(event.getGuild()); + settings.setDefaultPlaylist(pname); + event.reply(event.getClient().getSuccess()+" The default playlist for **"+event.getGuild().getName()+"** is now `"+pname+"`"); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/DebugCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/DebugCmd.java new file mode 100644 index 0000000..c6d0151 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/DebugCmd.java @@ -0,0 +1,68 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jdautilities.commons.JDAUtilitiesInfo; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary; +import net.dv8tion.jda.api.JDAInfo; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.ChannelType; + +public class DebugCmd extends OwnerCommand +{ + private final static String[] PROPERTIES = {"java.version", "java.vm.name", "java.vm.specification.version", + "java.runtime.name", "java.runtime.version", "java.specification.version", "os.arch", "os.name"}; + + private final Bot bot; + + public DebugCmd(Bot bot) + { + this.bot = bot; + this.name = "debug"; + this.help = "shows debug info"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + StringBuilder sb = new StringBuilder(); + sb.append("```\nSystem Properties:"); + for(String key: PROPERTIES) + sb.append("\n ").append(key).append(" = ").append(System.getProperty(key)); + sb.append("\n\nJMusicBot Information:") + .append("\n Version = ").append(OtherUtil.getCurrentVersion()) + .append("\n Owner = ").append(bot.getConfig().getOwnerId()) + .append("\n Prefix = ").append(bot.getConfig().getPrefix()) + .append("\n AltPrefix = ").append(bot.getConfig().getAltPrefix()) + .append("\n MaxSeconds = ").append(bot.getConfig().getMaxSeconds()) + .append("\n NPImages = ").append(bot.getConfig().useNPImages()) + .append("\n SongInStatus = ").append(bot.getConfig().getSongInStatus()) + .append("\n StayInChannel = ").append(bot.getConfig().getStay()) + .append("\n UseEval = ").append(bot.getConfig().useEval()) + .append("\n UpdateAlerts = ").append(bot.getConfig().useUpdateAlerts()); + sb.append("\n\nDependency Information:") + .append("\n JDA Version = ").append(JDAInfo.VERSION) + .append("\n JDA-Utilities Version = ").append(JDAUtilitiesInfo.VERSION) + .append("\n Lavaplayer Version = ").append(PlayerLibrary.VERSION); + long total = Runtime.getRuntime().totalMemory() / 1024 / 1024; + long used = total - (Runtime.getRuntime().freeMemory() / 1024 / 1024); + sb.append("\n\nRuntime Information:") + .append("\n Total Memory = ").append(total) + .append("\n Used Memory = ").append(used); + sb.append("\n\nDiscord Information:") + .append("\n ID = ").append(event.getJDA().getSelfUser().getId()) + .append("\n Guilds = ").append(event.getJDA().getGuildCache().size()) + .append("\n Users = ").append(event.getJDA().getUserCache().size()); + sb.append("\n```"); + + if(event.isFromType(ChannelType.PRIVATE) + || event.getSelfMember().hasPermission(event.getTextChannel(), Permission.MESSAGE_ATTACH_FILES)) + event.getChannel().sendFile(sb.toString().getBytes(), "debug_information.txt").queue(); + else + event.reply("Debug Information: " + sb.toString()); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java new file mode 100644 index 0000000..2e58f1c --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/EvalCmd.java @@ -0,0 +1,52 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import net.dv8tion.jda.api.entities.ChannelType; + +public class EvalCmd extends OwnerCommand +{ + private final Bot bot; + private final String engine; + + public EvalCmd(Bot bot) + { + this.bot = bot; + this.name = "eval"; + this.help = "evaluates nashorn code"; + this.aliases = bot.getConfig().getAliases(this.name); + this.engine = bot.getConfig().getEvalEngine(); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + ScriptEngine se = new ScriptEngineManager().getEngineByName(engine); + if(se == null) + { + event.replyError("The eval engine provided in the config (`"+engine+"`) doesn't exist. This could be due to an invalid " + + "engine name, or the engine not existing in your version of java (`"+System.getProperty("java.version")+"`)."); + return; + } + se.put("bot", bot); + se.put("event", event); + se.put("jda", event.getJDA()); + if (event.getChannelType() != ChannelType.PRIVATE) { + se.put("guild", event.getGuild()); + se.put("channel", event.getChannel()); + } + try + { + event.reply(event.getClient().getSuccess()+" Evaluated Successfully:\n```\n"+se.eval(event.getArgs())+" ```"); + } + catch(Exception e) + { + event.reply(event.getClient().getError()+" An exception was thrown:\n```\n"+e+" ```"); + } + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/PlaylistCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/PlaylistCmd.java new file mode 100644 index 0000000..88b5319 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/PlaylistCmd.java @@ -0,0 +1,204 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import java.io.IOException; +import java.util.List; +import com.jagrosh.jdautilities.command.Command; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import com.jagrosh.jmusicbot.playlist.PlaylistLoader.Playlist; + +public class PlaylistCmd extends OwnerCommand +{ + private final Bot bot; + public PlaylistCmd(Bot bot) + { + this.bot = bot; + this.guildOnly = false; + this.name = "playlist"; + this.arguments = ""; + this.help = "playlist management"; + this.aliases = bot.getConfig().getAliases(this.name); + this.children = new OwnerCommand[]{ + new ListCmd(), + new AppendlistCmd(), + new DeletelistCmd(), + new MakelistCmd(), + new DefaultlistCmd(bot) + }; + } + + @Override + public void execute(CommandEvent event) + { + StringBuilder builder = new StringBuilder(event.getClient().getWarning()+" Playlist Management Commands:\n"); + for(Command cmd: this.children) + builder.append("\n`").append(event.getClient().getPrefix()).append(name).append(" ").append(cmd.getName()) + .append(" ").append(cmd.getArguments()==null ? "" : cmd.getArguments()).append("` - ").append(cmd.getHelp()); + event.reply(builder.toString()); + } + + public class MakelistCmd extends OwnerCommand + { + public MakelistCmd() + { + this.name = "make"; + this.aliases = new String[]{"create"}; + this.help = "makes a new playlist"; + this.arguments = ""; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + String pname = event.getArgs().replaceAll("\\s+", "_"); + pname = pname.replaceAll("[*?|\\/\":<>]", ""); + if(pname == null || pname.isEmpty()) + { + event.replyError("Please provide a name for the playlist!"); + } + else if(bot.getPlaylistLoader().getPlaylist(pname) == null) + { + try + { + bot.getPlaylistLoader().createPlaylist(pname); + event.reply(event.getClient().getSuccess()+" Successfully created playlist `"+pname+"`!"); + } + catch(IOException e) + { + event.reply(event.getClient().getError()+" I was unable to create the playlist: "+e.getLocalizedMessage()); + } + } + else + event.reply(event.getClient().getError()+" Playlist `"+pname+"` already exists!"); + } + } + + public class DeletelistCmd extends OwnerCommand + { + public DeletelistCmd() + { + this.name = "delete"; + this.aliases = new String[]{"remove"}; + this.help = "deletes an existing playlist"; + this.arguments = ""; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + String pname = event.getArgs().replaceAll("\\s+", "_"); + if(bot.getPlaylistLoader().getPlaylist(pname)==null) + event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!"); + else + { + try + { + bot.getPlaylistLoader().deletePlaylist(pname); + event.reply(event.getClient().getSuccess()+" Successfully deleted playlist `"+pname+"`!"); + } + catch(IOException e) + { + event.reply(event.getClient().getError()+" I was unable to delete the playlist: "+e.getLocalizedMessage()); + } + } + } + } + + public class AppendlistCmd extends OwnerCommand + { + public AppendlistCmd() + { + this.name = "append"; + this.aliases = new String[]{"add"}; + this.help = "appends songs to an existing playlist"; + this.arguments = " | | ..."; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + String[] parts = event.getArgs().split("\\s+", 2); + if(parts.length<2) + { + event.reply(event.getClient().getError()+" Please include a playlist name and URLs to add!"); + return; + } + String pname = parts[0]; + Playlist playlist = bot.getPlaylistLoader().getPlaylist(pname); + if(playlist==null) + event.reply(event.getClient().getError()+" Playlist `"+pname+"` doesn't exist!"); + else + { + StringBuilder builder = new StringBuilder(); + playlist.getItems().forEach(item -> builder.append("\r\n").append(item)); + String[] urls = parts[1].split("\\|"); + for(String url: urls) + { + String u = url.trim(); + if(u.startsWith("<") && u.endsWith(">")) + u = u.substring(1, u.length()-1); + builder.append("\r\n").append(u); + } + try + { + bot.getPlaylistLoader().writePlaylist(pname, builder.toString()); + event.reply(event.getClient().getSuccess()+" Successfully added "+urls.length+" items to playlist `"+pname+"`!"); + } + catch(IOException e) + { + event.reply(event.getClient().getError()+" I was unable to append to the playlist: "+e.getLocalizedMessage()); + } + } + } + } + + public class DefaultlistCmd extends AutoplaylistCmd + { + public DefaultlistCmd(Bot bot) + { + super(bot); + this.name = "setdefault"; + this.aliases = new String[]{"default"}; + this.arguments = ""; + this.guildOnly = true; + } + } + + public class ListCmd extends OwnerCommand + { + public ListCmd() + { + this.name = "all"; + this.aliases = new String[]{"available","list"}; + this.help = "lists all available playlists"; + this.guildOnly = true; + } + + @Override + protected void execute(CommandEvent event) + { + if(!bot.getPlaylistLoader().folderExists()) + bot.getPlaylistLoader().createFolder(); + if(!bot.getPlaylistLoader().folderExists()) + { + event.reply(event.getClient().getWarning()+" Playlists folder does not exist and could not be created!"); + return; + } + List list = bot.getPlaylistLoader().getPlaylistNames(); + if(list==null) + event.reply(event.getClient().getError()+" Failed to load available playlists!"); + else if(list.isEmpty()) + event.reply(event.getClient().getWarning()+" There are no playlists in the Playlists folder!"); + else + { + StringBuilder builder = new StringBuilder(event.getClient().getSuccess()+" Available playlists:\n"); + list.forEach(str -> builder.append("`").append(str).append("` ")); + event.reply(builder.toString()); + } + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetavatarCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetavatarCmd.java new file mode 100644 index 0000000..0ba22be --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetavatarCmd.java @@ -0,0 +1,49 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import java.io.IOException; +import java.io.InputStream; +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import net.dv8tion.jda.api.entities.Icon; + +public class SetavatarCmd extends OwnerCommand +{ + public SetavatarCmd(Bot bot) + { + this.name = "setavatar"; + this.help = "sets the avatar of the bot"; + this.arguments = ""; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + String url; + if(event.getArgs().isEmpty()) + if(!event.getMessage().getAttachments().isEmpty() && event.getMessage().getAttachments().get(0).isImage()) + url = event.getMessage().getAttachments().get(0).getUrl(); + else + url = null; + else + url = event.getArgs(); + InputStream s = OtherUtil.imageFromUrl(url); + if(s==null) + { + event.reply(event.getClient().getError()+" Invalid or missing URL"); + } + else + { + try { + event.getSelfUser().getManager().setAvatar(Icon.from(s)).queue( + v -> event.reply(event.getClient().getSuccess()+" Successfully changed avatar."), + t -> event.reply(event.getClient().getError()+" Failed to set avatar.")); + } catch(IOException e) { + event.reply(event.getClient().getError()+" Could not load from provided URL."); + } + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetgameCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetgameCmd.java new file mode 100644 index 0000000..49bad2a --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetgameCmd.java @@ -0,0 +1,132 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import net.dv8tion.jda.api.entities.Activity; + +public class SetgameCmd extends OwnerCommand +{ + public SetgameCmd(Bot bot) + { + this.name = "setgame"; + this.help = "sets the game the bot is playing"; + this.arguments = "[action] [game]"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + this.children = new OwnerCommand[]{ + new SetlistenCmd(), + new SetstreamCmd(), + new SetwatchCmd() + }; + } + + @Override + protected void execute(CommandEvent event) + { + String title = event.getArgs().toLowerCase().startsWith("playing") ? event.getArgs().substring(7).trim() : event.getArgs(); + try + { + event.getJDA().getPresence().setActivity(title.isEmpty() ? null : Activity.playing(title)); + event.reply(event.getClient().getSuccess()+" **"+event.getSelfUser().getName() + +"** is "+(title.isEmpty() ? "no longer playing anything." : "now playing `"+title+"`")); + } + catch(Exception e) + { + event.reply(event.getClient().getError()+" The game could not be set!"); + } + } + + private class SetstreamCmd extends OwnerCommand + { + private SetstreamCmd() + { + this.name = "stream"; + this.aliases = new String[]{"twitch","streaming"}; + this.help = "sets the game the bot is playing to a stream"; + this.arguments = " "; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + String[] parts = event.getArgs().split("\\s+", 2); + if(parts.length<2) + { + event.replyError("Please include a twitch username and the name of the game to 'stream'"); + return; + } + try + { + event.getJDA().getPresence().setActivity(Activity.streaming(parts[1], "https://twitch.tv/"+parts[0])); + event.replySuccess("**"+event.getSelfUser().getName() + +"** is now streaming `"+parts[1]+"`"); + } + catch(Exception e) + { + event.reply(event.getClient().getError()+" The game could not be set!"); + } + } + } + + private class SetlistenCmd extends OwnerCommand + { + private SetlistenCmd() + { + this.name = "listen"; + this.aliases = new String[]{"listening"}; + this.help = "sets the game the bot is listening to"; + this.arguments = ""; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.replyError("Please include a title to listen to!"); + return; + } + String title = event.getArgs().toLowerCase().startsWith("to") ? event.getArgs().substring(2).trim() : event.getArgs(); + try + { + event.getJDA().getPresence().setActivity(Activity.listening(title)); + event.replySuccess("**"+event.getSelfUser().getName()+"** is now listening to `"+title+"`"); + } catch(Exception e) { + event.reply(event.getClient().getError()+" The game could not be set!"); + } + } + } + + private class SetwatchCmd extends OwnerCommand + { + private SetwatchCmd() + { + this.name = "watch"; + this.aliases = new String[]{"watching"}; + this.help = "sets the game the bot is watching"; + this.arguments = "<title>"; + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + if(event.getArgs().isEmpty()) + { + event.replyError("Please include a title to watch!"); + return; + } + String title = event.getArgs(); + try + { + event.getJDA().getPresence().setActivity(Activity.watching(title)); + event.replySuccess("**"+event.getSelfUser().getName()+"** is now watching `"+title+"`"); + } catch(Exception e) { + event.reply(event.getClient().getError()+" The game could not be set!"); + } + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetnameCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetnameCmd.java new file mode 100644 index 0000000..48537bf --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetnameCmd.java @@ -0,0 +1,37 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import net.dv8tion.jda.api.exceptions.RateLimitedException; + +public class SetnameCmd extends OwnerCommand +{ + public SetnameCmd(Bot bot) + { + this.name = "setname"; + this.help = "sets the name of the bot"; + this.arguments = "<name>"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + try + { + String oldname = event.getSelfUser().getName(); + event.getSelfUser().getManager().setName(event.getArgs()).complete(false); + event.reply(event.getClient().getSuccess()+" Name changed from `"+oldname+"` to `"+event.getArgs()+"`"); + } + catch(RateLimitedException e) + { + event.reply(event.getClient().getError()+" Name can only be changed twice per hour!"); + } + catch(Exception e) + { + event.reply(event.getClient().getError()+" That name is not valid!"); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetstatusCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetstatusCmd.java new file mode 100644 index 0000000..7d789cc --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/SetstatusCmd.java @@ -0,0 +1,37 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; +import net.dv8tion.jda.api.OnlineStatus; + +public class SetstatusCmd extends OwnerCommand +{ + public SetstatusCmd(Bot bot) + { + this.name = "setstatus"; + this.help = "sets the status the bot displays"; + this.arguments = "<status>"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + try { + OnlineStatus status = OnlineStatus.fromKey(event.getArgs()); + if(status==OnlineStatus.UNKNOWN) + { + event.replyError("Please include one of the following statuses: `ONLINE`, `IDLE`, `DND`, `INVISIBLE`"); + } + else + { + event.getJDA().getPresence().setStatus(status); + event.replySuccess("Set the status to `"+status.getKey().toUpperCase()+"`"); + } + } catch(Exception e) { + event.reply(event.getClient().getError()+" The status could not be set!"); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/commands/owner/ShutdownCmd.java b/src/main/java/com/jagrosh/jmusicbot/commands/owner/ShutdownCmd.java new file mode 100644 index 0000000..6718aee --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/commands/owner/ShutdownCmd.java @@ -0,0 +1,26 @@ +package com.jagrosh.jmusicbot.commands.owner; + +import com.jagrosh.jdautilities.command.CommandEvent; +import com.jagrosh.jmusicbot.Bot; +import com.jagrosh.jmusicbot.commands.OwnerCommand; + +public class ShutdownCmd extends OwnerCommand +{ + private final Bot bot; + + public ShutdownCmd(Bot bot) + { + this.bot = bot; + this.name = "shutdown"; + this.help = "safely shuts down"; + this.aliases = bot.getConfig().getAliases(this.name); + this.guildOnly = false; + } + + @Override + protected void execute(CommandEvent event) + { + event.replyWarning("Shutting down..."); + bot.shutdown(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/entities/Pair.java b/src/main/java/com/jagrosh/jmusicbot/entities/Pair.java new file mode 100644 index 0000000..30fa7c7 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/entities/Pair.java @@ -0,0 +1,23 @@ +package com.jagrosh.jmusicbot.entities; + +public class Pair<K,V> +{ + private final K key; + private final V value; + + public Pair(K key, V value) + { + this.key = key; + this.value = value; + } + + public K getKey() + { + return key; + } + + public V getValue() + { + return value; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/entities/Prompt.java b/src/main/java/com/jagrosh/jmusicbot/entities/Prompt.java new file mode 100644 index 0000000..aaab525 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/entities/Prompt.java @@ -0,0 +1,133 @@ +package com.jagrosh.jmusicbot.entities; + +import java.util.Scanner; +import javax.swing.JOptionPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Prompt +{ + private final String title; + private final String noguiMessage; + + private boolean nogui; + private boolean noprompt; + private Scanner scanner; + + public Prompt(String title) + { + this(title, null); + } + + public Prompt(String title, String noguiMessage) + { + this(title, noguiMessage, "true".equalsIgnoreCase(System.getProperty("nogui")), "true".equalsIgnoreCase(System.getProperty("noprompt"))); + } + + public Prompt(String title, String noguiMessage, boolean nogui, boolean noprompt) + { + this.title = title; + this.noguiMessage = noguiMessage == null ? "Switching to nogui mode. You can manually start in nogui mode by including the -Dnogui=true flag." : noguiMessage; + this.nogui = nogui; + this.noprompt = noprompt; + } + + public boolean isNoGUI() + { + return nogui; + } + + public void alert(Level level, String context, String message) + { + if(nogui) + { + Logger log = LoggerFactory.getLogger(context); + switch(level) + { + case INFO: + log.info(message); + break; + case WARNING: + log.warn(message); + break; + case ERROR: + log.error(message); + break; + default: + log.info(message); + break; + } + } + else + { + try + { + int option = 0; + switch(level) + { + case INFO: + option = JOptionPane.INFORMATION_MESSAGE; + break; + case WARNING: + option = JOptionPane.WARNING_MESSAGE; + break; + case ERROR: + option = JOptionPane.ERROR_MESSAGE; + break; + default: + option = JOptionPane.PLAIN_MESSAGE; + break; + } + JOptionPane.showMessageDialog(null, "<html><body><p style='width: 400px;'>"+message, title, option); + } + catch(Exception e) + { + nogui = true; + alert(Level.WARNING, context, noguiMessage); + alert(level, context, message); + } + } + } + + public String prompt(String content) + { + if(noprompt) + return null; + if(nogui) + { + if(scanner==null) + scanner = new Scanner(System.in); + try + { + System.out.println(content); + if(scanner.hasNextLine()) + return scanner.nextLine(); + return null; + } + catch(Exception e) + { + alert(Level.ERROR, title, "Unable to read input from command line."); + e.printStackTrace(); + return null; + } + } + else + { + try + { + return JOptionPane.showInputDialog(null, content, title, JOptionPane.QUESTION_MESSAGE); + } + catch(Exception e) + { + nogui = true; + alert(Level.WARNING, title, noguiMessage); + return prompt(content); + } + } + } + + public static enum Level + { + INFO, WARNING, ERROR; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/gui/ConsolePanel.java b/src/main/java/com/jagrosh/jmusicbot/gui/ConsolePanel.java new file mode 100644 index 0000000..d20ebd1 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/gui/ConsolePanel.java @@ -0,0 +1,30 @@ +package com.jagrosh.jmusicbot.gui; + +import java.awt.Dimension; +import java.awt.GridLayout; +import java.io.PrintStream; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +public class ConsolePanel extends JPanel { + + public ConsolePanel() + { + super(); + JTextArea text = new JTextArea(); + text.setLineWrap(true); + text.setWrapStyleWord(true); + text.setEditable(false); + PrintStream con=new PrintStream(new TextAreaOutputStream(text)); + System.setOut(con); + System.setErr(con); + + JScrollPane pane = new JScrollPane(); + pane.setViewportView(text); + + super.setLayout(new GridLayout(1,1)); + super.add(pane); + super.setPreferredSize(new Dimension(400,300)); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/gui/GUI.java b/src/main/java/com/jagrosh/jmusicbot/gui/GUI.java new file mode 100644 index 0000000..cc4be7e --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/gui/GUI.java @@ -0,0 +1,53 @@ +package com.jagrosh.jmusicbot.gui; + +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import javax.swing.JFrame; +import javax.swing.JTabbedPane; +import javax.swing.WindowConstants; +import com.jagrosh.jmusicbot.Bot; + +public class GUI extends JFrame +{ + private final ConsolePanel console; + private final Bot bot; + + public GUI(Bot bot) + { + super(); + this.bot = bot; + console = new ConsolePanel(); + } + + public void init() + { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setTitle("JMusicBot"); + JTabbedPane tabs = new JTabbedPane(); + tabs.add("Console", console); + getContentPane().add(tabs); + pack(); + setLocationRelativeTo(null); + setVisible(true); + addWindowListener(new WindowListener() + { + @Override public void windowOpened(WindowEvent e) { /* unused */ } + @Override public void windowClosing(WindowEvent e) + { + try + { + bot.shutdown(); + } + catch(Exception ex) + { + System.exit(0); + } + } + @Override public void windowClosed(WindowEvent e) { /* unused */ } + @Override public void windowIconified(WindowEvent e) { /* unused */ } + @Override public void windowDeiconified(WindowEvent e) { /* unused */ } + @Override public void windowActivated(WindowEvent e) { /* unused */ } + @Override public void windowDeactivated(WindowEvent e) { /* unused */ } + }); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/gui/TextAreaOutputStream.java b/src/main/java/com/jagrosh/jmusicbot/gui/TextAreaOutputStream.java new file mode 100644 index 0000000..46a2b43 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/gui/TextAreaOutputStream.java @@ -0,0 +1,139 @@ +package com.jagrosh.jmusicbot.gui; + +import java.awt.*; +import java.io.*; +import java.util.*; +import java.util.List; +import javax.swing.*; + +public class TextAreaOutputStream extends OutputStream { + +// ************************************************************************************************* +// INSTANCE MEMBERS +// ************************************************************************************************* + +private byte[] oneByte; // array for write(int val); +private Appender appender; // most recent action + +public TextAreaOutputStream(JTextArea txtara) { + this(txtara,1000); + } + +public TextAreaOutputStream(JTextArea txtara, int maxlin) { + if(maxlin<1) { throw new IllegalArgumentException("TextAreaOutputStream maximum lines must be positive (value="+maxlin+")"); } + oneByte=new byte[1]; + appender=new Appender(txtara,maxlin); + } + +/** Clear the current console text area. */ +public synchronized void clear() { + if(appender!=null) { appender.clear(); } + } + +@Override +public synchronized void close() { + appender=null; + } + +@Override +public synchronized void flush() { + /* empty */ + } + +@Override +public synchronized void write(int val) { + oneByte[0]=(byte)val; + write(oneByte,0,1); + } + +@Override +public synchronized void write(byte[] ba) { + write(ba,0,ba.length); + } + +@Override +public synchronized void write(byte[] ba,int str,int len) { + if(appender!=null) { appender.append(bytesToString(ba,str,len)); } + } + +//@edu.umd.cs.findbugs.annotations.SuppressWarnings("DM_DEFAULT_ENCODING") +static private String bytesToString(byte[] ba, int str, int len) { + try { + return new String(ba,str,len,"UTF-8"); + } catch(UnsupportedEncodingException thr) { + return new String(ba,str,len); + } // all JVMs are required to support UTF-8 + } + +// ************************************************************************************************* +// STATIC MEMBERS +// ************************************************************************************************* + + static class Appender + implements Runnable + { + static private final String EOL1="\n"; + static private final String EOL2=System.getProperty("line.separator",EOL1); + + private final JTextArea textArea; + private final int maxLines; // maximum lines allowed in text area + private final LinkedList<Integer> lengths; // length of lines within text area + private final List<String> values; // values waiting to be appended + + private int curLength; // length of current line + private boolean clear; + private boolean queue; + + Appender(JTextArea txtara, int maxlin) { + textArea =txtara; + maxLines =maxlin; + lengths =new LinkedList<>(); + values =new ArrayList<>(); + + curLength=0; + clear =false; + queue =true; + } + + private synchronized void append(String val) { + values.add(val); + if(queue) { + queue=false; + EventQueue.invokeLater(this); + } + } + + private synchronized void clear() { + clear=true; + curLength=0; + lengths.clear(); + values.clear(); + if(queue) { + queue=false; + EventQueue.invokeLater(this); + } + } + + // MUST BE THE ONLY METHOD THAT TOUCHES textArea! + @Override + public synchronized void run() { + if(clear) { textArea.setText(""); } + values.stream().map((val) -> { + curLength+=val.length(); + return val; + }).map((val) -> { + if(val.endsWith(EOL1) || val.endsWith(EOL2)) { + if(lengths.size()>=maxLines) { textArea.replaceRange("",0,lengths.removeFirst()); } + lengths.addLast(curLength); + curLength=0; + } + return val; + }).forEach((val) -> { + textArea.append(val); + }); + values.clear(); + clear =false; + queue =true; + } + } +} /* END PUBLIC CLASS */ diff --git a/src/main/java/com/jagrosh/jmusicbot/playlist/PlaylistLoader.java b/src/main/java/com/jagrosh/jmusicbot/playlist/PlaylistLoader.java new file mode 100644 index 0000000..feccb33 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/playlist/PlaylistLoader.java @@ -0,0 +1,276 @@ +package com.jagrosh.jmusicbot.playlist; + +import com.jagrosh.jmusicbot.BotConfig; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class PlaylistLoader +{ + private final BotConfig config; + + public PlaylistLoader(BotConfig config) + { + this.config = config; + } + + public List<String> getPlaylistNames() + { + if(folderExists()) + { + File folder = new File(OtherUtil.getPath(config.getPlaylistsFolder()).toString()); + return Arrays.asList(folder.listFiles((pathname) -> pathname.getName().endsWith(".txt"))) + .stream().map(f -> f.getName().substring(0,f.getName().length()-4)).collect(Collectors.toList()); + } + else + { + createFolder(); + return Collections.emptyList(); + } + } + + public void createFolder() + { + try + { + Files.createDirectory(OtherUtil.getPath(config.getPlaylistsFolder())); + } + catch (IOException ignore) {} + } + + public boolean folderExists() + { + return Files.exists(OtherUtil.getPath(config.getPlaylistsFolder())); + } + + public void createPlaylist(String name) throws IOException + { + Files.createFile(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt")); + } + + public void deletePlaylist(String name) throws IOException + { + Files.delete(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt")); + } + + public void writePlaylist(String name, String text) throws IOException + { + Files.write(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt"), text.trim().getBytes()); + } + + public Playlist getPlaylist(String name) + { + if(!getPlaylistNames().contains(name)) + return null; + try + { + if(folderExists()) + { + boolean[] shuffle = {false}; + List<String> list = new ArrayList<>(); + Files.readAllLines(OtherUtil.getPath(config.getPlaylistsFolder()+File.separator+name+".txt")).forEach(str -> + { + String s = str.trim(); + if(s.isEmpty()) + return; + if(s.startsWith("#") || s.startsWith("//")) + { + s = s.replaceAll("\\s+", ""); + if(s.equalsIgnoreCase("#shuffle") || s.equalsIgnoreCase("//shuffle")) + shuffle[0]=true; + } + else + list.add(s); + }); + if(shuffle[0]) + shuffle(list); + return new Playlist(name, list, shuffle[0]); + } + else + { + createFolder(); + return null; + } + } + catch(IOException e) + { + return null; + } + } + + + private static <T> void shuffle(List<T> list) + { + for(int first =0; first<list.size(); first++) + { + int second = (int)(Math.random()*list.size()); + T tmp = list.get(first); + list.set(first, list.get(second)); + list.set(second, tmp); + } + } + + + public class Playlist + { + private final String name; + private final List<String> items; + private final boolean shuffle; + private final List<AudioTrack> tracks = new LinkedList<>(); + private final List<PlaylistLoadError> errors = new LinkedList<>(); + private boolean loaded = false; + + private Playlist(String name, List<String> items, boolean shuffle) + { + this.name = name; + this.items = items; + this.shuffle = shuffle; + } + + public void loadTracks(AudioPlayerManager manager, Consumer<AudioTrack> consumer, Runnable callback) + { + if(loaded) + return; + loaded = true; + for(int i=0; i<items.size(); i++) + { + boolean last = i+1 == items.size(); + int index = i; + manager.loadItemOrdered(name, items.get(i), new AudioLoadResultHandler() + { + private void done() + { + if(last) + { + if(shuffle) + shuffleTracks(); + if(callback != null) + callback.run(); + } + } + + @Override + public void trackLoaded(AudioTrack at) + { + if(config.isTooLong(at)) + errors.add(new PlaylistLoadError(index, items.get(index), "This track is longer than the allowed maximum")); + else + { + at.setUserData(0L); + tracks.add(at); + consumer.accept(at); + } + done(); + } + + @Override + public void playlistLoaded(AudioPlaylist ap) + { + if(ap.isSearchResult()) + { + trackLoaded(ap.getTracks().get(0)); + } + else if(ap.getSelectedTrack()!=null) + { + trackLoaded(ap.getSelectedTrack()); + } + else + { + List<AudioTrack> loaded = new ArrayList<>(ap.getTracks()); + if(shuffle) + for(int first =0; first<loaded.size(); first++) + { + int second = (int)(Math.random()*loaded.size()); + AudioTrack tmp = loaded.get(first); + loaded.set(first, loaded.get(second)); + loaded.set(second, tmp); + } + loaded.removeIf(track -> config.isTooLong(track)); + loaded.forEach(at -> at.setUserData(0L)); + tracks.addAll(loaded); + loaded.forEach(at -> consumer.accept(at)); + } + done(); + } + + @Override + public void noMatches() + { + errors.add(new PlaylistLoadError(index, items.get(index), "No matches found.")); + done(); + } + + @Override + public void loadFailed(FriendlyException fe) + { + errors.add(new PlaylistLoadError(index, items.get(index), "Failed to load track: "+fe.getLocalizedMessage())); + done(); + } + }); + } + } + + public void shuffleTracks() + { + shuffle(tracks); + } + + public String getName() + { + return name; + } + + public List<String> getItems() + { + return items; + } + + public List<AudioTrack> getTracks() + { + return tracks; + } + + public List<PlaylistLoadError> getErrors() + { + return errors; + } + } + + public class PlaylistLoadError + { + private final int number; + private final String item; + private final String reason; + + private PlaylistLoadError(int number, String item, String reason) + { + this.number = number; + this.item = item; + this.reason = reason; + } + + public int getIndex() + { + return number; + } + + public String getItem() + { + return item; + } + + public String getReason() + { + return reason; + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/queue/AbstractQueue.java b/src/main/java/com/jagrosh/jmusicbot/queue/AbstractQueue.java new file mode 100644 index 0000000..46c78fb --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/queue/AbstractQueue.java @@ -0,0 +1,110 @@ +package com.jagrosh.jmusicbot.queue; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public abstract class AbstractQueue<T extends Queueable> +{ + protected AbstractQueue(AbstractQueue<T> queue) + { + this.list = queue != null ? queue.getList() : new LinkedList<>(); + } + + protected final List<T> list; + + public abstract int add(T item); + + public void addAt(int index, T item) + { + if(index >= list.size()) + list.add(item); + else + list.add(index, item); + } + + public int size() { + return list.size(); + } + + public T pull() { + return list.remove(0); + } + + public boolean isEmpty() + { + return list.isEmpty(); + } + + public List<T> getList() + { + return list; + } + + public T get(int index) { + return list.get(index); + } + + public T remove(int index) + { + return list.remove(index); + } + + public int removeAll(long identifier) + { + int count = 0; + for(int i=list.size()-1; i>=0; i--) + { + if(list.get(i).getIdentifier()==identifier) + { + list.remove(i); + count++; + } + } + return count; + } + + public void clear() + { + list.clear(); + } + + public int shuffle(long identifier) + { + List<Integer> iset = new ArrayList<>(); + for(int i=0; i<list.size(); i++) + { + if(list.get(i).getIdentifier()==identifier) + iset.add(i); + } + for(int j=0; j<iset.size(); j++) + { + int first = iset.get(j); + int second = iset.get((int)(Math.random()*iset.size())); + T temp = list.get(first); + list.set(first, list.get(second)); + list.set(second, temp); + } + return iset.size(); + } + + public void skip(int number) + { + if (number > 0) { + list.subList(0, number).clear(); + } + } + + /** + * Move an item to a different position in the list + * @param from The position of the item + * @param to The new position of the item + * @return the moved item + */ + public T moveItem(int from, int to) + { + T item = list.remove(from); + list.add(to, item); + return item; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/queue/FairQueue.java b/src/main/java/com/jagrosh/jmusicbot/queue/FairQueue.java new file mode 100644 index 0000000..f0e7f83 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/queue/FairQueue.java @@ -0,0 +1,34 @@ +package com.jagrosh.jmusicbot.queue; + +import java.util.HashSet; +import java.util.Set; + +public class FairQueue<T extends Queueable> extends AbstractQueue<T> +{ + public FairQueue(AbstractQueue<T> queue) + { + super(queue); + } + + protected final Set<Long> set = new HashSet<>(); + + @Override + public int add(T item) + { + int lastIndex; + for(lastIndex=list.size()-1; lastIndex>-1; lastIndex--) + if(list.get(lastIndex).getIdentifier() == item.getIdentifier()) + break; + lastIndex++; + set.clear(); + for(; lastIndex<list.size(); lastIndex++) + { + if(set.contains(list.get(lastIndex).getIdentifier())) + break; + set.add(list.get(lastIndex).getIdentifier()); + } + list.add(lastIndex, item); + return lastIndex; + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/queue/LinearQueue.java b/src/main/java/com/jagrosh/jmusicbot/queue/LinearQueue.java new file mode 100644 index 0000000..359d6f8 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/queue/LinearQueue.java @@ -0,0 +1,17 @@ +package com.jagrosh.jmusicbot.queue; + +public class LinearQueue<T extends Queueable> extends AbstractQueue<T> +{ + public LinearQueue(AbstractQueue<T> queue) + { + super(queue); + } + + @Override + public int add(T item) + { + list.add(item); + return list.size() - 1; + } + +} diff --git a/src/main/java/com/jagrosh/jmusicbot/queue/QueueSupplier.java b/src/main/java/com/jagrosh/jmusicbot/queue/QueueSupplier.java new file mode 100644 index 0000000..2ed85fa --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/queue/QueueSupplier.java @@ -0,0 +1,7 @@ +package com.jagrosh.jmusicbot.queue; + +@FunctionalInterface +public interface QueueSupplier +{ + <T extends Queueable> AbstractQueue<T> apply(AbstractQueue<T> queue); +} diff --git a/src/main/java/com/jagrosh/jmusicbot/queue/Queueable.java b/src/main/java/com/jagrosh/jmusicbot/queue/Queueable.java new file mode 100644 index 0000000..38130b2 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/queue/Queueable.java @@ -0,0 +1,6 @@ +package com.jagrosh.jmusicbot.queue; + +public interface Queueable { + + public long getIdentifier(); +} diff --git a/src/main/java/com/jagrosh/jmusicbot/settings/QueueType.java b/src/main/java/com/jagrosh/jmusicbot/settings/QueueType.java new file mode 100644 index 0000000..36bba69 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/settings/QueueType.java @@ -0,0 +1,50 @@ +package com.jagrosh.jmusicbot.settings; + +import com.jagrosh.jmusicbot.queue.AbstractQueue; +import com.jagrosh.jmusicbot.queue.FairQueue; +import com.jagrosh.jmusicbot.queue.LinearQueue; +import com.jagrosh.jmusicbot.queue.Queueable; +import com.jagrosh.jmusicbot.queue.QueueSupplier; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public enum QueueType +{ + LINEAR("\u23E9", "Linear", LinearQueue::new), // ⏩ + FAIR("\uD83D\uDD22", "Fair", FairQueue::new); // 🔢 + + private final String userFriendlyName; + private final String emoji; + private final QueueSupplier supplier; + + QueueType(final String emoji, final String userFriendlyName, QueueSupplier supplier) + { + this.userFriendlyName = userFriendlyName; + this.emoji = emoji; + this.supplier = supplier; + } + + public static List<String> getNames() + { + return Arrays.stream(QueueType.values()) + .map(type -> type.name().toLowerCase()) + .collect(Collectors.toList()); + } + + public <T extends Queueable> AbstractQueue<T> createInstance(AbstractQueue<T> previous) + { + return supplier.apply(previous); + } + + public String getUserFriendlyName() + { + return userFriendlyName; + } + + public String getEmoji() + { + return emoji; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/settings/RepeatMode.java b/src/main/java/com/jagrosh/jmusicbot/settings/RepeatMode.java new file mode 100644 index 0000000..e44b547 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/settings/RepeatMode.java @@ -0,0 +1,27 @@ +package com.jagrosh.jmusicbot.settings; + +public enum RepeatMode +{ + OFF(null, "Off"), + ALL("\uD83D\uDD01", "All"), // 🔁 + SINGLE("\uD83D\uDD02", "Single"); // 🔂 + + private final String emoji; + private final String userFriendlyName; + + private RepeatMode(String emoji, String userFriendlyName) + { + this.emoji = emoji; + this.userFriendlyName = userFriendlyName; + } + + public String getEmoji() + { + return emoji; + } + + public String getUserFriendlyName() + { + return userFriendlyName; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/settings/Settings.java b/src/main/java/com/jagrosh/jmusicbot/settings/Settings.java new file mode 100644 index 0000000..a0ab734 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/settings/Settings.java @@ -0,0 +1,179 @@ +package com.jagrosh.jmusicbot.settings; + +import com.jagrosh.jdautilities.command.GuildSettingsProvider; +import java.util.Collection; +import java.util.Collections; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.VoiceChannel; + +public class Settings implements GuildSettingsProvider +{ + private final SettingsManager manager; + protected long textId; + protected long voiceId; + protected long roleId; + private int volume; + private String defaultPlaylist; + private RepeatMode repeatMode; + private QueueType queueType; + private String prefix; + private double skipRatio; + + public Settings(SettingsManager manager, String textId, String voiceId, String roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType) + { + this.manager = manager; + try + { + this.textId = Long.parseLong(textId); + } + catch(NumberFormatException e) + { + this.textId = 0; + } + try + { + this.voiceId = Long.parseLong(voiceId); + } + catch(NumberFormatException e) + { + this.voiceId = 0; + } + try + { + this.roleId = Long.parseLong(roleId); + } + catch(NumberFormatException e) + { + this.roleId = 0; + } + this.volume = volume; + this.defaultPlaylist = defaultPlaylist; + this.repeatMode = repeatMode; + this.prefix = prefix; + this.skipRatio = skipRatio; + this.queueType = queueType; + } + + public Settings(SettingsManager manager, long textId, long voiceId, long roleId, int volume, String defaultPlaylist, RepeatMode repeatMode, String prefix, double skipRatio, QueueType queueType) + { + this.manager = manager; + this.textId = textId; + this.voiceId = voiceId; + this.roleId = roleId; + this.volume = volume; + this.defaultPlaylist = defaultPlaylist; + this.repeatMode = repeatMode; + this.prefix = prefix; + this.skipRatio = skipRatio; + this.queueType = queueType; + } + + // Getters + public TextChannel getTextChannel(Guild guild) + { + return guild == null ? null : guild.getTextChannelById(textId); + } + + public VoiceChannel getVoiceChannel(Guild guild) + { + return guild == null ? null : guild.getVoiceChannelById(voiceId); + } + + public Role getRole(Guild guild) + { + return guild == null ? null : guild.getRoleById(roleId); + } + + public int getVolume() + { + return volume; + } + + public String getDefaultPlaylist() + { + return defaultPlaylist; + } + + public RepeatMode getRepeatMode() + { + return repeatMode; + } + + public String getPrefix() + { + return prefix; + } + + public double getSkipRatio() + { + return skipRatio; + } + + public QueueType getQueueType() + { + return queueType; + } + + @Override + public Collection<String> getPrefixes() + { + return prefix == null ? Collections.emptySet() : Collections.singleton(prefix); + } + + // Setters + public void setTextChannel(TextChannel tc) + { + this.textId = tc == null ? 0 : tc.getIdLong(); + this.manager.writeSettings(); + } + + public void setVoiceChannel(VoiceChannel vc) + { + this.voiceId = vc == null ? 0 : vc.getIdLong(); + this.manager.writeSettings(); + } + + public void setDJRole(Role role) + { + this.roleId = role == null ? 0 : role.getIdLong(); + this.manager.writeSettings(); + } + + public void setVolume(int volume) + { + this.volume = volume; + this.manager.writeSettings(); + } + + public void setDefaultPlaylist(String defaultPlaylist) + { + this.defaultPlaylist = defaultPlaylist; + this.manager.writeSettings(); + } + + public void setRepeatMode(RepeatMode mode) + { + this.repeatMode = mode; + this.manager.writeSettings(); + } + + public void setPrefix(String prefix) + { + this.prefix = prefix; + this.manager.writeSettings(); + } + + public void setSkipRatio(double skipRatio) + { + this.skipRatio = skipRatio; + this.manager.writeSettings(); + } + + public void setQueueType(QueueType queueType) + { + this.queueType = queueType; + this.manager.writeSettings(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/settings/SettingsManager.java b/src/main/java/com/jagrosh/jmusicbot/settings/SettingsManager.java new file mode 100644 index 0000000..650f2af --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/settings/SettingsManager.java @@ -0,0 +1,116 @@ +package com.jagrosh.jmusicbot.settings; + +import com.jagrosh.jdautilities.command.GuildSettingsManager; +import com.jagrosh.jmusicbot.utils.OtherUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.util.HashMap; +import net.dv8tion.jda.api.entities.Guild; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SettingsManager implements GuildSettingsManager<Settings> +{ + private final static Logger LOG = LoggerFactory.getLogger("Settings"); + private final static String SETTINGS_FILE = "serversettings.json"; + private final HashMap<Long,Settings> settings; + + public SettingsManager() + { + this.settings = new HashMap<>(); + + try { + JSONObject loadedSettings = new JSONObject(new String(Files.readAllBytes(OtherUtil.getPath(SETTINGS_FILE)))); + loadedSettings.keySet().forEach((id) -> { + JSONObject o = loadedSettings.getJSONObject(id); + + // Legacy version support: On versions 0.3.3 and older, the repeat mode was represented as a boolean. + if (!o.has("repeat_mode") && o.has("repeat") && o.getBoolean("repeat")) + o.put("repeat_mode", RepeatMode.ALL); + + + settings.put(Long.parseLong(id), new Settings(this, + o.has("text_channel_id") ? o.getString("text_channel_id") : null, + o.has("voice_channel_id")? o.getString("voice_channel_id") : null, + o.has("dj_role_id") ? o.getString("dj_role_id") : null, + o.has("volume") ? o.getInt("volume") : 100, + o.has("default_playlist")? o.getString("default_playlist") : null, + o.has("repeat_mode") ? o.getEnum(RepeatMode.class, "repeat_mode"): RepeatMode.OFF, + o.has("prefix") ? o.getString("prefix") : null, + o.has("skip_ratio") ? o.getDouble("skip_ratio") : -1, + o.has("queue_type") ? o.getEnum(QueueType.class, "queue_type") : QueueType.FAIR)); + }); + } catch (NoSuchFileException e) { + // create an empty json file + try { + LOG.info("serversettings.json will be created in " + OtherUtil.getPath("serversettings.json").toAbsolutePath()); + Files.write(OtherUtil.getPath("serversettings.json"), new JSONObject().toString(4).getBytes()); + } catch(IOException ex) { + LOG.warn("Failed to create new settings file: "+ex); + } + return; + } catch(IOException | JSONException e) { + LOG.warn("Failed to load server settings: "+e); + } + + LOG.info("serversettings.json loaded from " + OtherUtil.getPath("serversettings.json").toAbsolutePath()); + } + + /** + * Gets non-null settings for a Guild + * + * @param guild the guild to get settings for + * @return the existing settings, or new settings for that guild + */ + @Override + public Settings getSettings(Guild guild) + { + return getSettings(guild.getIdLong()); + } + + public Settings getSettings(long guildId) + { + return settings.computeIfAbsent(guildId, id -> createDefaultSettings()); + } + + private Settings createDefaultSettings() + { + return new Settings(this, 0, 0, 0, 100, null, RepeatMode.OFF, null, -1, QueueType.FAIR); + } + + protected void writeSettings() + { + JSONObject obj = new JSONObject(); + settings.keySet().stream().forEach(key -> { + JSONObject o = new JSONObject(); + Settings s = settings.get(key); + if(s.textId!=0) + o.put("text_channel_id", Long.toString(s.textId)); + if(s.voiceId!=0) + o.put("voice_channel_id", Long.toString(s.voiceId)); + if(s.roleId!=0) + o.put("dj_role_id", Long.toString(s.roleId)); + if(s.getVolume()!=100) + o.put("volume",s.getVolume()); + if(s.getDefaultPlaylist() != null) + o.put("default_playlist", s.getDefaultPlaylist()); + if(s.getRepeatMode()!=RepeatMode.OFF) + o.put("repeat_mode", s.getRepeatMode()); + if(s.getPrefix() != null) + o.put("prefix", s.getPrefix()); + if(s.getSkipRatio() != -1) + o.put("skip_ratio", s.getSkipRatio()); + if(s.getQueueType() != QueueType.FAIR) + o.put("queue_type", s.getQueueType().name()); + obj.put(Long.toString(key), o); + }); + try { + Files.write(OtherUtil.getPath(SETTINGS_FILE), obj.toString(4).getBytes()); + } catch(IOException ex){ + LOG.warn("Failed to write to file: "+ex); + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java new file mode 100644 index 0000000..8dd53f4 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/utils/FormatUtil.java @@ -0,0 +1,93 @@ +package com.jagrosh.jmusicbot.utils; + +import com.jagrosh.jmusicbot.audio.RequestMetadata.UserInfo; +import java.util.List; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.VoiceChannel; + +public class FormatUtil { + + public static String formatUsername(String username, String discrim) + { + if(discrim == null || discrim.equals("0000")) + { + return username; + } + else + { + return username + "#" + discrim; + } + } + + public static String formatUsername(UserInfo userinfo) + { + return formatUsername(userinfo.username, userinfo.discrim); + } + + public static String formatUsername(User user) + { + return formatUsername(user.getName(), user.getDiscriminator()); + } + + public static String progressBar(double percent) + { + String str = ""; + for(int i=0; i<12; i++) + if(i == (int)(percent*12)) + str+="\uD83D\uDD18"; // 🔘 + else + str+="▬"; + return str; + } + + public static String volumeIcon(int volume) + { + if(volume == 0) + return "\uD83D\uDD07"; // 🔇 + if(volume < 30) + return "\uD83D\uDD08"; // 🔈 + if(volume < 70) + return "\uD83D\uDD09"; // 🔉 + return "\uD83D\uDD0A"; // 🔊 + } + + public static String listOfTChannels(List<TextChannel> list, String query) + { + String out = " Multiple text channels found matching \""+query+"\":"; + for(int i=0; i<6 && i<list.size(); i++) + out+="\n - "+list.get(i).getName()+" (<#"+list.get(i).getId()+">)"; + if(list.size()>6) + out+="\n**And "+(list.size()-6)+" more...**"; + return out; + } + + public static String listOfVChannels(List<VoiceChannel> list, String query) + { + String out = " Multiple voice channels found matching \""+query+"\":"; + for(int i=0; i<6 && i<list.size(); i++) + out+="\n - "+list.get(i).getAsMention()+" (ID:"+list.get(i).getId()+")"; + if(list.size()>6) + out+="\n**And "+(list.size()-6)+" more...**"; + return out; + } + + public static String listOfRoles(List<Role> list, String query) + { + String out = " Multiple roles found matching \""+query+"\":"; + for(int i=0; i<6 && i<list.size(); i++) + out+="\n - "+list.get(i).getName()+" (ID:"+list.get(i).getId()+")"; + if(list.size()>6) + out+="\n**And "+(list.size()-6)+" more...**"; + return out; + } + + public static String filter(String input) + { + return input.replace("\u202E","") + .replace("@everyone", "@\u0435veryone") // cyrillic letter e + .replace("@here", "@h\u0435re") // cyrillic letter e + .trim(); + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/OtherUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/OtherUtil.java new file mode 100644 index 0000000..9847563 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/utils/OtherUtil.java @@ -0,0 +1,214 @@ +package com.jagrosh.jmusicbot.utils; + +import com.jagrosh.jmusicbot.JMusicBot; +import com.jagrosh.jmusicbot.entities.Prompt; +import java.io.*; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Path; +import java.nio.file.Paths; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.ApplicationInfo; +import net.dv8tion.jda.api.entities.User; +import okhttp3.*; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +public class OtherUtil +{ + public final static String NEW_VERSION_AVAILABLE = "Update available!\n" + + "Current version: %s\n" + + "New Version: %s\n\n" + + "Please visit https://code.cif.su/anthony/CiF-Music-Bot/releases/latest to get the latest release."; + private final static String WINDOWS_INVALID_PATH = "c:\\windows\\system32\\"; + + /** + * gets a Path from a String + * also fixes the windows tendency to try to start in system32 + * any time the bot tries to access this path, it will instead start in the location of the jar file + * + * @param path the string path + * @return the Path object + */ + public static Path getPath(String path) + { + Path result = Paths.get(path); + // special logic to prevent trying to access system32 + if(result.toAbsolutePath().toString().toLowerCase().startsWith(WINDOWS_INVALID_PATH)) + { + try + { + result = Paths.get(new File(JMusicBot.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath() + File.separator + path); + } + catch(URISyntaxException ignored) {} + } + return result; + } + + /** + * Loads a resource from the jar as a string + * + * @param clazz class base object + * @param name name of resource + * @return string containing the contents of the resource + */ + public static String loadResource(Object clazz, String name) + { + try(BufferedReader reader = new BufferedReader(new InputStreamReader(clazz.getClass().getResourceAsStream(name)))) + { + StringBuilder sb = new StringBuilder(); + reader.lines().forEach(line -> sb.append("\r\n").append(line)); + return sb.toString().trim(); + } + catch(IOException ignored) + { + return null; + } + } + + /** + * Loads image data from a URL + * + * @param url url of image + * @return inputstream of url + */ + public static InputStream imageFromUrl(String url) + { + if(url==null) + return null; + try + { + URL u = new URL(url); + URLConnection urlConnection = u.openConnection(); + urlConnection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36"); + return urlConnection.getInputStream(); + } + catch(IOException | IllegalArgumentException ignore) {} + return null; + } + + /** + * Parses an activity from a string + * + * @param game the game, including the action such as 'playing' or 'watching' + * @return the parsed activity + */ + public static Activity parseGame(String game) + { + if(game==null || game.trim().isEmpty() || game.trim().equalsIgnoreCase("default")) + return null; + String lower = game.toLowerCase(); + if(lower.startsWith("playing")) + return Activity.playing(makeNonEmpty(game.substring(7).trim())); + if(lower.startsWith("listening to")) + return Activity.listening(makeNonEmpty(game.substring(12).trim())); + if(lower.startsWith("listening")) + return Activity.listening(makeNonEmpty(game.substring(9).trim())); + if(lower.startsWith("watching")) + return Activity.watching(makeNonEmpty(game.substring(8).trim())); + if(lower.startsWith("streaming")) + { + String[] parts = game.substring(9).trim().split("\\s+", 2); + if(parts.length == 2) + { + return Activity.streaming(makeNonEmpty(parts[1]), "https://twitch.tv/"+parts[0]); + } + } + return Activity.playing(game); + } + + public static String makeNonEmpty(String str) + { + return str == null || str.isEmpty() ? "\u200B" : str; + } + + public static OnlineStatus parseStatus(String status) + { + if(status==null || status.trim().isEmpty()) + return OnlineStatus.ONLINE; + OnlineStatus st = OnlineStatus.fromKey(status); + return st == null ? OnlineStatus.ONLINE : st; + } + + public static void checkJavaVersion(Prompt prompt) + { + if(!System.getProperty("java.vm.name").contains("64")) + prompt.alert(Prompt.Level.WARNING, "Java Version", + "It appears that you may not be using a supported Java version. Please use 64-bit java."); + } + + public static void checkVersion(Prompt prompt) + { + // Get current version number + String version = getCurrentVersion(); + + // Check for new version + String latestVersion = getLatestVersion(); + + if(latestVersion!=null && !latestVersion.equals(version)) + { + prompt.alert(Prompt.Level.WARNING, "JMusicBot Version", String.format(NEW_VERSION_AVAILABLE, version, latestVersion)); + } + } + + public static String getCurrentVersion() + { + if(JMusicBot.class.getPackage()!=null && JMusicBot.class.getPackage().getImplementationVersion()!=null) + return JMusicBot.class.getPackage().getImplementationVersion(); + else + return "UNKNOWN"; + } + + public static String getLatestVersion() + { + try + { + Response response = new OkHttpClient.Builder().build() + .newCall(new Request.Builder().get().url("https://api.github.com/repos/jagrosh/MusicBot/releases/latest").build()) + .execute(); + ResponseBody body = response.body(); + if(body != null) + { + try(Reader reader = body.charStream()) + { + JSONObject obj = new JSONObject(new JSONTokener(reader)); + return obj.getString("tag_name"); + } + finally + { + response.close(); + } + } + else + return null; + } + catch(IOException | JSONException | NullPointerException ex) + { + return null; + } + } + + /** + * Checks if the bot JMusicBot is being run on is supported & returns the reason if it is not. + * @return A string with the reason, or null if it is supported. + */ + public static String getUnsupportedBotReason(JDA jda) + { + if (jda.getSelfUser().getFlags().contains(User.UserFlag.VERIFIED_BOT)) + return "The bot is verified. Using JMusicBot in a verified bot is not supported."; + + ApplicationInfo info = jda.retrieveApplicationInfo().complete(); + if (info.isBotPublic()) + return "\"Public Bot\" is enabled. Using JMusicBot as a public bot is not supported. Please disable it in the " + + "Developer Dashboard at https://discord.com/developers/applications/" + jda.getSelfUser().getId() + "/bot ." + + "You may also need to disable all Installation Contexts at https://discord.com/developers/applications/" + + jda.getSelfUser().getId() + "/installation ."; + + return null; + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java new file mode 100644 index 0000000..c3eed53 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/utils/TimeUtil.java @@ -0,0 +1,123 @@ +package com.jagrosh.jmusicbot.utils; + +public class TimeUtil +{ + + public static String formatTime(long duration) + { + if(duration == Long.MAX_VALUE) + return "LIVE"; + long seconds = Math.round(duration/1000.0); + long hours = seconds/(60*60); + seconds %= 60*60; + long minutes = seconds/60; + seconds %= 60; + return (hours>0 ? hours+":" : "") + (minutes<10 ? "0"+minutes : minutes) + ":" + (seconds<10 ? "0"+seconds : seconds); + } + + /** + * Parses a seek time string into milliseconds and determines if it's relative. + * Supports "colon time" (HH:MM:SS) or "unit time" (1h20m) + * @param args time string + * @return SeekTime object, or null if the string could not be parsed + */ + public static SeekTime parseTime(String args) + { + if (args.length() == 0) return null; + String timestamp = args; + boolean relative = false; // seek forward or backward + boolean isSeekingBackwards = false; + char first = timestamp.charAt(0); + if (first == '+' || first == '-') + { + relative = true; + isSeekingBackwards = first == '-'; + timestamp = timestamp.substring(1); + } + + long milliseconds = parseColonTime(timestamp); + if(milliseconds == -1) milliseconds = parseUnitTime(timestamp); + if(milliseconds == -1) return null; + + milliseconds *= isSeekingBackwards ? -1 : 1; + + return new SeekTime(milliseconds, relative); + } + + /** + * @param timestamp timestamp formatted as: [+ | -] <HH:MM:SS | MM:SS | SS> + * @return Time in milliseconds + */ + public static long parseColonTime(String timestamp) + { + String[] timestampSplitArray = timestamp.split(":+"); + if(timestampSplitArray.length > 3 ) + return -1; + double[] timeUnitArray = new double[3]; // hours, minutes, seconds + for(int index = 0; index < timestampSplitArray.length; index++) + { + String unit = timestampSplitArray[index]; + if (unit.startsWith("+") || unit.startsWith("-")) return -1; + unit = unit.replace(",", "."); + try + { + timeUnitArray[index + 3 - timestampSplitArray.length] = Double.parseDouble(unit); + } + catch (NumberFormatException e) + { + return -1; + } + } + return Math.round(timeUnitArray[0] * 3600000 + timeUnitArray[1] * 60000 + timeUnitArray[2] * 1000); + } + + /** + * + * @param timestr time string formatted as a unit time, e.g. 20m10, 1d5h20m14s or 1h and 20m + * @return Time in milliseconds + */ + public static long parseUnitTime(String timestr) + { + timestr = timestr.replaceAll("(?i)(\\s|,|and)","") + .replaceAll("(?is)(-?\\d+|[a-z]+)", "$1 ") + .trim(); + String[] vals = timestr.split("\\s+"); + int time = 0; + try + { + for(int j=0; j<vals.length; j+=2) + { + int num = Integer.parseInt(vals[j]); + + if(vals.length > j+1) + { + if(vals[j+1].toLowerCase().startsWith("m")) + num*=60; + else if(vals[j+1].toLowerCase().startsWith("h")) + num*=60*60; + else if(vals[j+1].toLowerCase().startsWith("d")) + num*=60*60*24; + } + + time+=num*1000; + } + } + catch(Exception ex) + { + return -1; + } + return time; + } + + public static class SeekTime + { + public final long milliseconds; + public final boolean relative; + + private SeekTime(long milliseconds, boolean relative) + { + this.milliseconds = milliseconds; + this.relative = relative; + } + } +} diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/YouTubeUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/YouTubeUtil.java new file mode 100644 index 0000000..d26dbc6 --- /dev/null +++ b/src/main/java/com/jagrosh/jmusicbot/utils/YouTubeUtil.java @@ -0,0 +1,46 @@ +package com.jagrosh.jmusicbot.utils; + +import com.sedmelluq.lava.extensions.youtuberotator.planner.*; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block; +import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block; + +import java.util.List; + +public class YouTubeUtil { + public enum RoutingPlanner { + NONE, + ROTATE_ON_BAN, + LOAD_BALANCE, + NANO_SWITCH, + ROTATING_NANO_SWITCH + } + + public static IpBlock parseIpBlock(String cidr) { + if (Ipv6Block.isIpv6CidrBlock(cidr)) + return new Ipv6Block(cidr); + + if (Ipv4Block.isIpv4CidrBlock(cidr)) + return new Ipv4Block(cidr); + + throw new IllegalArgumentException("Could not parse CIDR " + cidr); + } + + public static AbstractRoutePlanner createRouterPlanner(RoutingPlanner routingPlanner, List<IpBlock> ipBlocks) { + + switch (routingPlanner) { + case NONE: + return null; + case ROTATE_ON_BAN: + return new RotatingIpRoutePlanner(ipBlocks); + case LOAD_BALANCE: + return new BalancingIpRoutePlanner(ipBlocks); + case NANO_SWITCH: + return new NanoIpRoutePlanner(ipBlocks, true); + case ROTATING_NANO_SWITCH: + return new RotatingNanoIpRoutePlanner(ipBlocks); + default: + throw new IllegalArgumentException("Unknown RoutingPlanner value provided"); + } + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..8985f56 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,16 @@ +<configuration> + + <appender name="Simple" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <!-- Pattern --> + <pattern> + %nopex[%d{HH:mm:ss}] [%level] [%logger{0}]: %msg%n%ex + </pattern> + </encoder> + </appender> + + <root level="INFO"> + <appender-ref ref="Simple"/> + </root> + +</configuration> diff --git a/src/main/resources/natives/linux-aarch32/libconnector.so b/src/main/resources/natives/linux-aarch32/libconnector.so new file mode 100644 index 0000000..85d41f0 Binary files /dev/null and b/src/main/resources/natives/linux-aarch32/libconnector.so differ diff --git a/src/main/resources/natives/linux-aarch64/libconnector.so b/src/main/resources/natives/linux-aarch64/libconnector.so new file mode 100644 index 0000000..abf337a Binary files /dev/null and b/src/main/resources/natives/linux-aarch64/libconnector.so differ diff --git a/src/main/resources/natives/linux-arm/libconnector.so b/src/main/resources/natives/linux-arm/libconnector.so new file mode 100644 index 0000000..0668792 Binary files /dev/null and b/src/main/resources/natives/linux-arm/libconnector.so differ diff --git a/src/main/resources/natives/linux-armhf/libconnector.so b/src/main/resources/natives/linux-armhf/libconnector.so new file mode 100644 index 0000000..0668792 Binary files /dev/null and b/src/main/resources/natives/linux-armhf/libconnector.so differ diff --git a/src/main/resources/natives/linux-x86/libconnector.so b/src/main/resources/natives/linux-x86/libconnector.so new file mode 100644 index 0000000..41b0012 Binary files /dev/null and b/src/main/resources/natives/linux-x86/libconnector.so differ diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf new file mode 100644 index 0000000..e99c2fc --- /dev/null +++ b/src/main/resources/reference.conf @@ -0,0 +1,251 @@ +//-----------------------------------------------------// +// Most stuff here is optional, but you must set the // +// owner-id and the bot-token // +//-----------------------------------------------------// + + +// This sets the token for the bot to log in with +// This MUST be a bot token (user tokens will not work) +// If you don't know how to get a bot token, please see the guide here: +// https://github.com/jagrosh/MusicBot/wiki/Getting-a-Bot-Token + +token = BOT_TOKEN_HERE + + +// This sets the owner of the bot +// This needs to be the owner's ID (a 17-18 digit number) +// https://github.com/jagrosh/MusicBot/wiki/Finding-Your-User-ID + +owner = 0 // OWNER ID + + +// This sets the prefix for the bot +// The prefix is used to control the commands +// If you use !!, the play command will be !!play +// If you do not set this, the prefix will be a mention of the bot (@Botname play) + +prefix = "@mention" + + +// If you set this, it modifies the default game of the bot +// Set this to NONE to have no game +// Set this to DEFAULT to use the default game +// You can make the game "Playing X", "Listening to X", or "Watching X" +// where X is the title. If you don't include an action, it will use the +// default of "Playing" + +game = "DEFAULT" + + +// If you set this, it will modify the default status of bot +// Valid values: ONLINE IDLE DND INVISIBLE + +status = ONLINE + + +// If you set this to true, the bot will list the title of the song it is currently playing in its +// "Playing" status. Note that this will ONLY work if the bot is playing music on ONE guild; +// if the bot is playing on multiple guilds, this will not work. + +songinstatus=false + + +// If you set this, the bot will also use this prefix in addition to +// the one provided above + +altprefix = "NONE" + + +// If you set these, it will change the various emojis + +success = "🎶" +warning = "💡" +error = "🚫" +loading = "⌚" +searching = "🔎" + + +// If you set this, you change the word used to view the help. +// For example, if you set the prefix to !! and the help to cmds, you would type +// !!cmds to see the help text + +help = help + + +// If you set this, the "nowplaying" command will show youtube thumbnails +// Note: If you set this to true, the nowplaying boxes will NOT refresh +// This is because refreshing the boxes causes the image to be reloaded +// every time it refreshes. + +npimages = false + + +// If you set this, the bot will not leave a voice channel after it finishes a queue. +// Keep in mind that being connected to a voice channel uses additional bandwith, +// so this option is not recommended if bandwidth is a concern. + +stayinchannel = false + + +// This sets the maximum amount of seconds any track loaded can be. If not set or set +// to any number less than or equal to zero, there is no maximum time length. This time +// restriction applies to songs loaded from any source. + +maxtime = 0 + +// This sets the maximum number of pages of songs that can be loaded from a YouTube +// playlist. Each page can contain up to 100 tracks. Playing a playlist with more +// pages than the maximum will stop loading after the provided number of pages. +// For example, if the max was set to 15 and a playlist contained 1850 tracks, +// only the first 1500 tracks (15 pages) would be loaded. By default, this is +// set to 10 pages (1000 tracks). + +maxytplaylistpages = 10 + + +// This sets the ratio of users that must vote to skip the currently playing song. +// Guild owners can define their own skip ratios, but this will be used if a guild +// has not defined their own skip ratio. + +skipratio = 0.55 + + +// This sets the amount of seconds the bot will stay alone on a voice channel until it +// automatically leaves the voice channel and clears the queue. If not set or set +// to any number less than or equal to zero, the bot won't leave when alone. + +alonetimeuntilstop = 0 + + +// This sets an alternative folder to be used as the Playlists folder +// This can be a relative or absolute path + +playlistsfolder = "Playlists" + + +// By default, the bot will DM the owner if the bot is running and a new version of the bot +// becomes available. Set this to false to disable this feature. + +updatealerts=true + + +// Changing this changes the lyrics provider +// Currently available providers: "A-Z Lyrics", "Genius", "MusicMatch", "LyricsFreak" +// At the time of writing, I would recommend sticking with A-Z Lyrics or MusicMatch, +// as Genius tends to have a lot of non-song results and you might get something +// completely unrelated to what you want. +// If you are interested in contributing a provider, please see +// https://github.com/jagrosh/JLyrics + +lyrics.default = "A-Z Lyrics" + + +// These settings allow you to configure custom aliases for all commands. +// Multiple aliases may be given, separated by commas. +// +// Example 1: Giving command "play" the alias "p": +// play = [ p ] +// +// Example 2: Giving command "search" the aliases "yts" and "find": +// search = [ yts, find ] + +aliases { + // General commands + settings = [ status ] + + // Music commands + lyrics = [] + nowplaying = [ np, current ] + play = [] + playlists = [ pls ] + queue = [ list ] + remove = [ delete ] + scsearch = [] + search = [ ytsearch ] + shuffle = [] + skip = [ voteskip ] + + // Admin commands + prefix = [ setprefix ] + setdj = [] + setskip = [ setskippercent, skippercent, setskipratio ] + settc = [] + setvc = [] + + // DJ Commands + forceremove = [ forcedelete, modremove, moddelete, modelete ] + forceskip = [ modskip ] + movetrack = [ move ] + pause = [] + playnext = [] + queuetype = [] + repeat = [] + skipto = [ jumpto ] + stop = [ leave ] + volume = [ vol ] +} + + +// This sets the logging verbosity. +// Available levels: off, error, warn, info, debug, trace, all +// +// It is recommended to leave this at info. Debug log levels might help with troubleshooting, +// but can contain sensitive data. + +loglevel = info + + +// Transforms are used to modify specific play inputs and convert them to different kinds of inputs +// These are quite complicated to use, and have limited use-cases, but in theory allow for rough +// whitelists or blacklists, roundabout loading from some sources, and customization of how things are +// requested. +// +// These are NOT EASY to set up, so if you want to use these, you'll need to look through the code +// for how they work and what fields are needed. Also, it's possible this feature might get entirely +// removed in the future if I find a better way to do this. + +transforms = {} + + +// This sets the "Proof of Origin Token" and visitor data to use when playing back YouTube videos. +// It is recommend to obtain a PO token to avoid getting IP banned from YouTube. + +ytpotoken = "PO_TOKEN_HERE" +ytvisitordata = "VISITOR_DATA_HERE" + + +// This configures the routing planner to use for IP rotation on YouTube. +// When set, you need to also set an IP block below. +// The following routing strategies are available: +// NONE = Disable +// ROTATE_ON_BAN = Switch IP when currently used address gets banned. +// LOAD_BALANCE = Selects random IP for each track playback. +// NANO_SWITCH = Selects IPv6 address based on current nanosecond. Recommended for one /64 IPv6 blocks. +// ROTATING_NANO_SWITCH = Same as above, but uses the next /64 IPv6 block in the list in case of a ban. + +ytroutingplanner = NONE + + +// The IPv4 and IPv6 blocks to use when using the above routing planner. +// This does nothing when ytroutingplanner is set to none. +// The list accepts a list of IPv4 or IPv6 CIDR blocks (not a mix of them however) +// IPv4 example: [ "192.0.2.183", "198.51.100.153/29" ] +// IPv6 example: [ "2a02:1234:5678:9abc::/64" ] + +ytipblocks = [ ] + + + +// If you set this to true, it will enable the eval command for the bot owner. This command +// allows the bot owner to run arbitrary code from the bot's account. +// +// WARNING: +// This command can be extremely dangerous. If you don't know what you're doing, you could +// cause horrific problems on your Discord server or on whatever computer this bot is running +// on. Never run this command unless you are completely positive what you are running. +// +// DO NOT ENABLE THIS IF YOU DON'T KNOW WHAT THIS DOES OR HOW TO USE IT +// IF SOMEONE ASKS YOU TO ENABLE THIS, THERE IS AN 11/10 CHANCE THEY ARE TRYING TO SCAM YOU + +eval=false +evalengine="Nashorn" diff --git a/src/test/java/com/jagrosh/jmusicbot/FairQueueTest.java b/src/test/java/com/jagrosh/jmusicbot/FairQueueTest.java new file mode 100644 index 0000000..b3878e1 --- /dev/null +++ b/src/test/java/com/jagrosh/jmusicbot/FairQueueTest.java @@ -0,0 +1,45 @@ +package com.jagrosh.jmusicbot; + +import com.jagrosh.jmusicbot.queue.FairQueue; +import com.jagrosh.jmusicbot.queue.Queueable; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FairQueueTest +{ + @Test + public void differentIdentifierSize() + { + FairQueue<Q> queue = new FairQueue<>(null); + int size = 100; + for(int i=0; i<size; i++) + queue.add(new Q(i)); + assertEquals(queue.size(), size); + } + + @Test + public void sameIdentifierSize() + { + FairQueue<Q> queue = new FairQueue<>(null); + int size = 100; + for(int i=0; i<size; i++) + queue.add(new Q(0)); + assertEquals(queue.size(), size); + } + + private class Q implements Queueable + { + private final long identifier; + + private Q(long identifier) + { + this.identifier = identifier; + } + + @Override + public long getIdentifier() + { + return identifier; + } + } +} diff --git a/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java b/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java new file mode 100644 index 0000000..bfcab65 --- /dev/null +++ b/src/test/java/com/jagrosh/jmusicbot/TimeUtilTest.java @@ -0,0 +1,100 @@ +package com.jagrosh.jmusicbot; + +import com.jagrosh.jmusicbot.utils.TimeUtil; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TimeUtilTest +{ + @Test + public void singleDigit() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("5"); + assertNotNull(seek); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void multipleDigits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("99:9:999"); + assertNotNull(seek); + assertEquals(357939000, seek.milliseconds); + + seek = TimeUtil.parseTime("99h9m999s"); + assertNotNull(seek); + assertEquals(357939000, seek.milliseconds); + } + + @Test + public void decimalDigits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("99.5:9.0:999.777"); + assertNotNull(seek); + assertEquals(359739777, seek.milliseconds); + } + + @Test + public void seeking() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("5"); + assertNotNull(seek); + assertFalse(seek.relative); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void relativeSeekingForward() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("+5"); + assertNotNull(seek); + assertTrue(seek.relative); + assertEquals(5000, seek.milliseconds); + } + + @Test + public void relativeSeekingBackward() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("-5"); + assertNotNull(seek); + assertTrue(seek.relative); + assertEquals(-5000, seek.milliseconds); + } + + @Test + public void parseTimeArgumentLength() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime(""); + assertNull(seek); + } + + @Test + public void timestampTotalUnits() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:1:1"); + assertNull(seek); + + seek = TimeUtil.parseTime("1h2m3m4s5s"); + assertNotNull(seek); + assertEquals(3909000, seek.milliseconds); + } + + @Test + public void relativeSymbol() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("+-1:-+1:+-1"); + assertNull(seek); + } + + @Test + public void timestampNumberFormat() + { + TimeUtil.SeekTime seek = TimeUtil.parseTime("1:1:a"); + assertNull(seek); + + seek = TimeUtil.parseTime("1a2s"); + assertNotNull(seek); + assertEquals(3000, seek.milliseconds); + } +}