Version 1: Basic functionality #1
|
@ -1,4 +1,11 @@
|
|||
# HandieBot
|
||||
Discord Bot for music and other utilities, built with Discord4J and Lavaplayer.
|
||||
|
||||
HandieBot is a bot for Discord, written using Java, the [Discord4J](https://github.com/austinv11/Discord4J) library, and the [Lavaplayer](https://github.com/sedmelluq/lavaplayer) library for sound processing. It is a fully-fledged bot with the ability to manage playlists, get music from URLs, and perform other necessary functions for a clean and enjoyable user experience.
|
||||
|
||||
## Commands
|
||||
|
||||
### `play <URL>`
|
||||
|
||||
Issuing the `play` command attempts to load a song from a given URL, and append it to the active queue. The bot will tell the user quite obviously if their link does not work, or if there was an internal error. If there are already some songs in the queue, then this will also, if successful, tell the user approximately how long it will be until their song is played.
|
||||
|
||||
|
||||
|
|
13
pom.xml
13
pom.xml
|
@ -13,14 +13,18 @@
|
|||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.7</source>
|
||||
<target>1.7</target>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<pastebin4j.version>1.1.0</pastebin4j.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jcenter</id>
|
||||
|
@ -43,6 +47,11 @@
|
|||
<artifactId>lavaplayer</artifactId>
|
||||
<version>1.2.39</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.kennedyoliveira</groupId>
|
||||
<artifactId>pastebin4j</artifactId>
|
||||
<version>${pastebin4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -1,149 +1,68 @@
|
|||
package handiebot;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import handiebot.command.CommandHandler;
|
||||
import handiebot.lavaplayer.GuildMusicManager;
|
||||
import handiebot.lavaplayer.MusicPlayer;
|
||||
import handiebot.view.BotLog;
|
||||
import handiebot.view.BotWindow;
|
||||
import handiebot.view.View;
|
||||
import sx.blah.discord.api.ClientBuilder;
|
||||
import sx.blah.discord.api.IDiscordClient;
|
||||
import sx.blah.discord.api.events.EventSubscriber;
|
||||
import sx.blah.discord.handle.audio.IAudioManager;
|
||||
import sx.blah.discord.handle.impl.events.ReadyEvent;
|
||||
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IVoiceChannel;
|
||||
import sx.blah.discord.util.DiscordException;
|
||||
import sx.blah.discord.util.MissingPermissionsException;
|
||||
import sx.blah.discord.util.RateLimitException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* Main Class for the discord bot. Contains client loading information and general event processing.
|
||||
*/
|
||||
public class HandieBot {
|
||||
|
||||
public static final String APPLICATION_NAME = "HandieBot";
|
||||
private static final String TOKEN = "MjgzNjUyOTg5MjEyNjg4Mzg0.C45A_Q.506b0G6my1FEFa7_YY39lxLBHUY";
|
||||
|
||||
private static IDiscordClient client;
|
||||
public static View view;
|
||||
private static BotWindow window;
|
||||
public static BotLog log;
|
||||
|
||||
private CommandHandler commandHandler;
|
||||
private static CommandHandler commandHandler;
|
||||
public static MusicPlayer musicPlayer;
|
||||
|
||||
@EventSubscriber
|
||||
public void onMessageReceived(MessageReceivedEvent event) {
|
||||
commandHandler.handleCommand(event);
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public void onReady(ReadyEvent event){
|
||||
log.log(BotLog.TYPE.INFO, "HandieBot initialized.");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws DiscordException, RateLimitException {
|
||||
System.out.println("Logging bot in...");
|
||||
|
||||
musicPlayer = new MusicPlayer();
|
||||
|
||||
view = new View();
|
||||
log = new BotLog(view.getOutputArea());
|
||||
window = new BotWindow(view);
|
||||
|
||||
log.log(BotLog.TYPE.INFO, "Logging client in...");
|
||||
client = new ClientBuilder().withToken(TOKEN).build();
|
||||
client.getDispatcher().registerListener(new HandieBot());
|
||||
client.login();
|
||||
}
|
||||
|
||||
private final AudioPlayerManager playerManager;
|
||||
private final Map<Long, GuildMusicManager> musicManagers;
|
||||
|
||||
private HandieBot() {
|
||||
this.musicManagers = new HashMap<>();
|
||||
this.playerManager = new DefaultAudioPlayerManager();
|
||||
AudioSourceManagers.registerRemoteSources(playerManager);
|
||||
AudioSourceManagers.registerLocalSource(playerManager);
|
||||
|
||||
this.commandHandler = new CommandHandler(this);
|
||||
/**
|
||||
* Safely shuts down the bot on all guilds.
|
||||
*/
|
||||
public static void quit(){
|
||||
log.log(BotLog.TYPE.INFO, "Shutting down the bot.");
|
||||
musicPlayer.quitAll();
|
||||
client.logout();
|
||||
window.dispose();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private synchronized GuildMusicManager getGuildAudioPlayer(IGuild guild) {
|
||||
long guildId = Long.parseLong(guild.getID());
|
||||
GuildMusicManager musicManager = musicManagers.get(guildId);
|
||||
|
||||
if (musicManager == null) {
|
||||
musicManager = new GuildMusicManager(playerManager);
|
||||
musicManagers.put(guildId, musicManager);
|
||||
}
|
||||
|
||||
guild.getAudioManager().setAudioProvider(musicManager.getAudioProvider());
|
||||
|
||||
return musicManager;
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public void onMessageReceived(MessageReceivedEvent event) {
|
||||
this.commandHandler.handleCommand(event);
|
||||
}
|
||||
|
||||
public void loadAndPlay(final IChannel channel, final String trackUrl) {
|
||||
GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
|
||||
|
||||
playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler() {
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack track) {
|
||||
sendMessageToChannel(channel, "Adding to queue " + track.getInfo().title);
|
||||
|
||||
play(channel.getGuild(), musicManager, track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist playlist) {
|
||||
AudioTrack firstTrack = playlist.getSelectedTrack();
|
||||
|
||||
if (firstTrack == null) {
|
||||
firstTrack = playlist.getTracks().get(0);
|
||||
}
|
||||
|
||||
sendMessageToChannel(channel, "Adding to queue " + firstTrack.getInfo().title + " (first track of playlist " + playlist.getName() + ")");
|
||||
|
||||
play(channel.getGuild(), musicManager, firstTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
sendMessageToChannel(channel, "Nothing found by " + trackUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException exception) {
|
||||
sendMessageToChannel(channel, "Could not play: " + exception.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void play(IGuild guild, GuildMusicManager musicManager, AudioTrack track) {
|
||||
connectToFirstVoiceChannel(guild.getAudioManager());
|
||||
|
||||
musicManager.scheduler.queue(track);
|
||||
}
|
||||
|
||||
public void skipTrack(IChannel channel) {
|
||||
GuildMusicManager musicManager = getGuildAudioPlayer(channel.getGuild());
|
||||
musicManager.scheduler.nextTrack();
|
||||
|
||||
sendMessageToChannel(channel, "Skipped to next track.");
|
||||
}
|
||||
|
||||
private void sendMessageToChannel(IChannel channel, String message) {
|
||||
try {
|
||||
channel.sendMessage(message);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static void connectToFirstVoiceChannel(IAudioManager audioManager) {
|
||||
for (IVoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) {
|
||||
if (voiceChannel.isConnected()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (IVoiceChannel voiceChannel : audioManager.getGuild().getVoiceChannels()) {
|
||||
try {
|
||||
voiceChannel.join();
|
||||
} catch (MissingPermissionsException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
package handiebot.command;
|
||||
|
||||
import com.sun.istack.internal.NotNull;
|
||||
import handiebot.HandieBot;
|
||||
import handiebot.utils.DisappearingMessage;
|
||||
import handiebot.view.BotLog;
|
||||
import handiebot.view.actions.QuitAction;
|
||||
import handiebot.view.actions.music.PlayAction;
|
||||
import handiebot.view.actions.music.QueueListAction;
|
||||
import handiebot.view.actions.music.SkipAction;
|
||||
import handiebot.view.actions.music.ToggleRepeatAction;
|
||||
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
|
||||
import sx.blah.discord.handle.obj.*;
|
||||
import sx.blah.discord.util.EmbedBuilder;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
import static handiebot.HandieBot.log;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* Class to process commands.
|
||||
*/
|
||||
public class CommandHandler {
|
||||
|
||||
private static String PREFIX = "!";
|
||||
|
||||
private final HandieBot bot;
|
||||
|
||||
public CommandHandler(HandieBot bot){
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
public static String PREFIX = "!";
|
||||
/**
|
||||
* Main method to handle user messages.
|
||||
* @param event The event generated by the message.
|
||||
*/
|
||||
public void handleCommand(MessageReceivedEvent event){
|
||||
public static void handleCommand(MessageReceivedEvent event){
|
||||
IMessage message = event.getMessage();
|
||||
IUser user = event.getAuthor();
|
||||
IChannel channel = event.getChannel();
|
||||
|
@ -34,10 +35,41 @@ public class CommandHandler {
|
|||
String command = extractCommand(message);
|
||||
String[] args = extractArgs(message);
|
||||
if (guild != null && command != null){
|
||||
if (command.equals("play") && args.length == 1){
|
||||
this.bot.loadAndPlay(channel, args[0]);
|
||||
DisappearingMessage.deleteMessageAfter(1000, message);
|
||||
if (command.equals("play")){
|
||||
//Play or queue a song.
|
||||
new PlayAction(guild, args).actionPerformed(null);
|
||||
} else if (command.equals("skip") && args.length == 0){
|
||||
//Skip the current song.
|
||||
new SkipAction(guild).actionPerformed(null);
|
||||
} else if (command.equals("help")){
|
||||
this.sendHelpInfo(user);
|
||||
//Send a PM to the user with help info.
|
||||
sendHelpInfo(user);//TODO finish the help command and fill in with new descriptions each time.
|
||||
} else if (command.equals("queue")){
|
||||
//Display the first few items of the queue.
|
||||
new QueueListAction(guild, (args.length == 1) && args[0].equals("all")).actionPerformed(null);
|
||||
} else if (command.equals("repeat")){
|
||||
//Toggle repeat.
|
||||
new ToggleRepeatAction(guild).actionPerformed(null);
|
||||
} else if (command.equals("clear")){
|
||||
//TODO clear command.
|
||||
} else if (command.equals("quit")){
|
||||
//Quit the application.
|
||||
new QuitAction(guild).actionPerformed(null);
|
||||
} else if (command.equals("playlist")){
|
||||
//Do playlist actions.
|
||||
//TODO perform actions!
|
||||
} else if (command.equals("prefix") && args.length == 1){
|
||||
//Set the prefix to the first argument.
|
||||
if (args[0].length() != 1){
|
||||
new DisappearingMessage(channel, "You may only set the prefix to 1 character. To do otherwise is simply foolish.", 3000);
|
||||
} else {
|
||||
new DisappearingMessage(channel, "Command prefix set to "+PREFIX, 10000);
|
||||
log.log(BotLog.TYPE.INFO, guild, "Prefix set to "+PREFIX);
|
||||
setPrefix(args[0]);
|
||||
}
|
||||
} else {
|
||||
log.log(BotLog.TYPE.ERROR, guild, "Invalid command: "+command+" issued by "+user.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +79,7 @@ public class CommandHandler {
|
|||
* @param message The message to get a command from.
|
||||
* @return The command word, minus the prefix, or null.
|
||||
*/
|
||||
private String extractCommand(IMessage message){
|
||||
private static String extractCommand(IMessage message){
|
||||
String[] words = message.getContent().split(" ");
|
||||
if (words[0].startsWith(PREFIX)){
|
||||
return words[0].replaceFirst(PREFIX, "").toLowerCase();
|
||||
|
@ -61,7 +93,7 @@ public class CommandHandler {
|
|||
* @return A list of strings representing args.
|
||||
*/
|
||||
@NotNull
|
||||
private String[] extractArgs(IMessage message){
|
||||
private static String[] extractArgs(IMessage message){
|
||||
String[] words = message.getContent().split(" ");
|
||||
if (words[0].startsWith(PREFIX)){
|
||||
String[] args = new String[words.length-1];
|
||||
|
@ -77,7 +109,7 @@ public class CommandHandler {
|
|||
* Method to send a useful list of commands to any user if they desire.
|
||||
* @param user The user to send the message to.
|
||||
*/
|
||||
private void sendHelpInfo(IUser user){
|
||||
private static void sendHelpInfo(IUser user){
|
||||
IPrivateChannel pm = user.getOrCreatePMChannel();
|
||||
EmbedBuilder builder = new EmbedBuilder();
|
||||
|
||||
|
@ -96,7 +128,7 @@ public class CommandHandler {
|
|||
* Sets the prefix used to identify commands.
|
||||
* @param prefix The prefix appended to the beginning of commands.
|
||||
*/
|
||||
public void setPrefix(String prefix){
|
||||
public static void setPrefix(String prefix){
|
||||
PREFIX = prefix;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package handiebot.lavaplayer;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class AudioLoadResultHandler implements com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler {
|
||||
|
||||
private TrackScheduler scheduler;
|
||||
|
||||
public AudioLoadResultHandler(TrackScheduler scheduler){
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack audioTrack) {
|
||||
System.out.println("Adding to queue "+ audioTrack.getInfo().title);
|
||||
scheduler.queue(audioTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist audioPlaylist) {
|
||||
System.out.println("Adding playlist to queue.");
|
||||
audioPlaylist.getTracks().forEach(track -> this.scheduler.queue(track));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
System.out.println("No matches!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException e) {
|
||||
System.out.println("Load failed.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package handiebot.lavaplayer;
|
|||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
|
@ -13,9 +14,9 @@ public class GuildMusicManager {
|
|||
|
||||
public final TrackScheduler scheduler;
|
||||
|
||||
public GuildMusicManager(AudioPlayerManager manager){
|
||||
public GuildMusicManager(AudioPlayerManager manager, IGuild guild){
|
||||
this.player = manager.createPlayer();
|
||||
this.scheduler = new TrackScheduler(this.player);
|
||||
this.scheduler = new TrackScheduler(this.player, guild, manager);
|
||||
this.player.addListener(this.scheduler);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
package handiebot.lavaplayer;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
|
||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
|
||||
import handiebot.command.CommandHandler;
|
||||
import handiebot.utils.DisappearingMessage;
|
||||
import handiebot.view.BotLog;
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IMessage;
|
||||
import sx.blah.discord.handle.obj.IVoiceChannel;
|
||||
import sx.blah.discord.util.EmbedBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static handiebot.HandieBot.log;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* This class is a container for all the music related functions, and contains methods for easy playback and queue
|
||||
* management.
|
||||
*/
|
||||
public class MusicPlayer {
|
||||
|
||||
//Name for the message and voice channels dedicated to this bot.
|
||||
static String CHANNEL_NAME = "HandieBotMusic";
|
||||
private static String PASTEBIN_KEY = "769adc01154922ece448cabd7a33b57c";
|
||||
|
||||
private final AudioPlayerManager playerManager;
|
||||
|
||||
/*
|
||||
Mappings of music managers, channels and voice channels for each guild.
|
||||
*/
|
||||
private Map<IGuild, GuildMusicManager> musicManagers;
|
||||
private Map<IGuild, IChannel> chatChannels;
|
||||
private Map<IGuild, IVoiceChannel> voiceChannels;
|
||||
|
||||
public MusicPlayer(){
|
||||
//Initialize player manager.
|
||||
this.playerManager = new DefaultAudioPlayerManager();
|
||||
AudioSourceManagers.registerLocalSource(playerManager);
|
||||
AudioSourceManagers.registerRemoteSources(playerManager);
|
||||
|
||||
//Initialize all maps.
|
||||
this.musicManagers = new HashMap<>();
|
||||
this.chatChannels = new HashMap<>();
|
||||
this.voiceChannels = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the music manager specific to a particular guild.
|
||||
* @param guild The guild to get the music manager for.
|
||||
* @return The music manager for a guild.
|
||||
*/
|
||||
private GuildMusicManager getMusicManager(IGuild guild){
|
||||
if (!this.musicManagers.containsKey(guild)){
|
||||
log.log(BotLog.TYPE.MUSIC, guild, "Creating new music manager and audio provider for guild: "+guild.getName());
|
||||
this.musicManagers.put(guild, new GuildMusicManager(this.playerManager, guild));
|
||||
guild.getAudioManager().setAudioProvider(this.musicManagers.get(guild).getAudioProvider());
|
||||
}
|
||||
return this.musicManagers.get(guild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the chat channel specific to a particular guild. This channel is used send updates about playback and
|
||||
* responses to people's commands. If none exists, the bot will attempt to make a channel.
|
||||
* @param guild The guild to get the channel from.
|
||||
* @return A message channel on a particular guild that is specifically for music.
|
||||
*/
|
||||
public IChannel getChatChannel(IGuild guild){
|
||||
if (!this.chatChannels.containsKey(guild)){
|
||||
List<IChannel> channels = guild.getChannelsByName(CHANNEL_NAME.toLowerCase());
|
||||
if (channels.isEmpty()){
|
||||
log.log(BotLog.TYPE.MUSIC, guild, "No chat channel found, creating a new one.");
|
||||
this.chatChannels.put(guild, guild.createChannel(CHANNEL_NAME.toLowerCase()));
|
||||
} else {
|
||||
this.chatChannels.put(guild, channels.get(0));
|
||||
}
|
||||
}
|
||||
return this.chatChannels.get(guild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the voice channel associated with a particular guild. This channel is used for audio playback. If none
|
||||
* exists, the bot will attempt to make a voice channel.
|
||||
* @param guild The guild to get the channel from.
|
||||
* @return The voice channel on a guild that is for this bot.
|
||||
*/
|
||||
public IVoiceChannel getVoiceChannel(IGuild guild){
|
||||
if (!this.voiceChannels.containsKey(guild)){
|
||||
List<IVoiceChannel> channels = guild.getVoiceChannelsByName(CHANNEL_NAME);
|
||||
if (channels.isEmpty()){
|
||||
log.log(BotLog.TYPE.MUSIC, guild, "No voice channel found, creating a new one.");
|
||||
this.voiceChannels.put(guild, guild.createVoiceChannel(CHANNEL_NAME));
|
||||
} else {
|
||||
this.voiceChannels.put(guild, channels.get(0));
|
||||
}
|
||||
}
|
||||
return this.voiceChannels.get(guild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the repeating of songs for a particular guild.
|
||||
* @param guild The guild to repeat for.
|
||||
*/
|
||||
public void toggleRepeat(IGuild guild){
|
||||
GuildMusicManager musicManager = this.getMusicManager(guild);
|
||||
|
||||
musicManager.scheduler.setRepeat(!musicManager.scheduler.isRepeating());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a formatted message to the guild about the first few items in a queue.
|
||||
*/
|
||||
public void showQueueList(IGuild guild, boolean showAll){
|
||||
List<AudioTrack> tracks = getMusicManager(guild).scheduler.queueList();
|
||||
if (tracks.size() == 0) {
|
||||
new DisappearingMessage(getChatChannel(guild), "The queue is empty. Use **"+ CommandHandler.PREFIX+"play** *URL* to add songs.", 3000);
|
||||
} else {
|
||||
if (!showAll) {
|
||||
EmbedBuilder builder = new EmbedBuilder();
|
||||
builder.withColor(255, 0, 0);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < (tracks.size() <= 10 ? tracks.size() : 10); i++) {
|
||||
sb.append(i + 1);
|
||||
sb.append(". ");
|
||||
sb.append('[');
|
||||
sb.append(tracks.get(i).getInfo().title);
|
||||
sb.append("](");
|
||||
sb.append(tracks.get(i).getInfo().uri);
|
||||
sb.append(")");
|
||||
int seconds = (int) (tracks.get(i).getInfo().length / 1000);
|
||||
int minutes = seconds / 60;
|
||||
seconds = seconds % 60;
|
||||
String time = String.format(" [%d:%02d]\n", minutes, seconds);
|
||||
sb.append(time);
|
||||
}
|
||||
builder.withTimestamp(System.currentTimeMillis());
|
||||
builder.appendField("Showing " + (tracks.size() <= 10 ? tracks.size() : "the first 10") + " track" + (tracks.size() > 1 ? "s" : "") + ".", sb.toString(), false);
|
||||
IMessage message = getChatChannel(guild).sendMessage(builder.build());
|
||||
DisappearingMessage.deleteMessageAfter(6000, message);
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder("Queue for Discord Server: "+guild.getName()+"\n");
|
||||
for (int i = 0; i < tracks.size(); i++){
|
||||
sb.append(i+1).append(". ").append(tracks.get(i).getInfo().title);
|
||||
int seconds = (int) (tracks.get(i).getInfo().length / 1000);
|
||||
int minutes = seconds / 60;
|
||||
seconds = seconds % 60;
|
||||
String time = String.format(" [%d:%02d]\n", minutes, seconds);
|
||||
sb.append(time);
|
||||
}
|
||||
//TODO: get pastebin working.
|
||||
/*
|
||||
PasteBin pasteBin = new PasteBin(new AccountCredentials(PASTEBIN_KEY));
|
||||
Paste paste = new Paste(PASTEBIN_KEY);
|
||||
paste.setTitle("Music Queue for Discord Server: "+guild.getName());
|
||||
paste.setContent(sb.toString());
|
||||
paste.setExpiration(PasteExpiration.ONE_HOUR);
|
||||
paste.setVisibility(PasteVisibility.PUBLIC);
|
||||
final String pasteURL = pasteBin.createPaste(paste);
|
||||
log.log(BotLog.TYPE.INFO, guild, "Uploaded full queue to "+pasteURL);
|
||||
new DisappearingMessage(getChatChannel(guild), "You may view the full queue here. "+pasteURL, 60000);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a URL to the queue, or outputs an error message if it fails.
|
||||
* @param trackURL A string representing a youtube/soundcloud URL.
|
||||
*/
|
||||
public void loadToQueue(IGuild guild, String trackURL){
|
||||
this.playerManager.loadItemOrdered(getMusicManager(guild), trackURL, new AudioLoadResultHandler() {
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack audioTrack) {
|
||||
addToQueue(guild, audioTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist audioPlaylist) {
|
||||
if (audioPlaylist.getTracks().size() > 0){
|
||||
AudioTrack firstTrack = audioPlaylist.getSelectedTrack();
|
||||
if (firstTrack == null){
|
||||
firstTrack = audioPlaylist.getTracks().get(0);
|
||||
}
|
||||
addToQueue(guild, firstTrack);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
log.log(BotLog.TYPE.ERROR, guild, "No matches found for: "+trackURL);
|
||||
new DisappearingMessage(getChatChannel(guild), "Unable to find a result for: "+trackURL, 3000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException e) {
|
||||
log.log(BotLog.TYPE.ERROR, guild, "Unable to load song: "+trackURL+". "+e.getMessage());
|
||||
new DisappearingMessage(getChatChannel(guild), "Unable to load. "+e.getMessage(), 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a track to the queue and sends a message to the appropriate channel notifying users.
|
||||
* @param track The track to queue.
|
||||
*/
|
||||
private void addToQueue(IGuild guild, AudioTrack track){
|
||||
IVoiceChannel voiceChannel = getVoiceChannel(guild);
|
||||
if (voiceChannel != null){
|
||||
if (!voiceChannel.isConnected()) {
|
||||
voiceChannel.join();
|
||||
}
|
||||
long timeUntilPlay = getMusicManager(guild).scheduler.getTimeUntilDone();
|
||||
getMusicManager(guild).scheduler.queue(track);
|
||||
//Build message.
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (timeUntilPlay > 0) {
|
||||
sb.append("Added **").append(track.getInfo().title).append("** to the queue.");
|
||||
}
|
||||
//If there's some tracks in the queue, get the time until this one plays.
|
||||
if (timeUntilPlay > 0){
|
||||
sb.append(String.format("\nTime until play: %d min, %d sec",
|
||||
TimeUnit.MILLISECONDS.toMinutes(timeUntilPlay),
|
||||
TimeUnit.MILLISECONDS.toSeconds(timeUntilPlay) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeUntilPlay))
|
||||
));
|
||||
}
|
||||
IMessage message = getChatChannel(guild).sendMessage(sb.toString());
|
||||
DisappearingMessage.deleteMessageAfter(3000, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If possible, try to begin playing from the track scheduler's queue.
|
||||
*/
|
||||
public void playQueue(IGuild guild){
|
||||
IVoiceChannel vc = this.getVoiceChannel(guild);
|
||||
if (!vc.isConnected()){
|
||||
vc.join();
|
||||
}
|
||||
getMusicManager(guild).scheduler.nextTrack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current track.
|
||||
*/
|
||||
public void skipTrack(IGuild guild){
|
||||
getMusicManager(guild).scheduler.nextTrack();
|
||||
log.log(BotLog.TYPE.MUSIC, guild, "Skipping the current track. ");
|
||||
new DisappearingMessage(getChatChannel(guild), "Skipping the current track.", 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops playback and disconnects from the voice channel, to cease music actions.
|
||||
* @param guild The guild to quit from.
|
||||
*/
|
||||
public void quit(IGuild guild){
|
||||
getMusicManager(guild).scheduler.quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the same functions as quit, but with every guild.
|
||||
*/
|
||||
public void quitAll(){
|
||||
this.musicManagers.forEach((guild, musicManager) -> {
|
||||
musicManager.scheduler.quit();
|
||||
});
|
||||
this.playerManager.shutdown();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
package handiebot.lavaplayer;
|
||||
|
||||
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 handiebot.view.BotLog;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static handiebot.HandieBot.log;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* A Playlist is a list of AudioTracks which a track scheduler can pull from to create a queue filled with songs. The
|
||||
* playlist is persistent, i.e. it is saved into a file.
|
||||
*/
|
||||
public class Playlist {
|
||||
|
||||
private String name;
|
||||
private long creatorUID;
|
||||
|
||||
private List<AudioTrack> tracks;
|
||||
|
||||
/**
|
||||
* Creates an empty playlist template.
|
||||
* @param name The name of the playlist.
|
||||
* @param creatorUID The ID of the user who created it.
|
||||
*/
|
||||
public Playlist(String name, long creatorUID){
|
||||
this.name = name;
|
||||
this.creatorUID = creatorUID;
|
||||
this.tracks = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a playlist from a file with the given name.
|
||||
* @param name The name of the file.
|
||||
*/
|
||||
public Playlist(String name, AudioPlayerManager playerManager){
|
||||
this.name = name;
|
||||
this.load(playerManager);
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public long getCreatorUID(){
|
||||
return this.creatorUID;
|
||||
}
|
||||
|
||||
public List<AudioTrack> getTracks(){
|
||||
return this.tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a track to the end of the playlist.
|
||||
* @param track The track to add.
|
||||
*/
|
||||
public void addTrack(AudioTrack track){
|
||||
this.tracks.add(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a track from the playlist.
|
||||
* @param track The track to remove.
|
||||
*/
|
||||
public void removeTrack(AudioTrack track){
|
||||
this.tracks.remove(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all tracks from a specified playlist to this one.
|
||||
* @param other The other playlist to make a copy of.
|
||||
*/
|
||||
public void copyFrom(Playlist other){
|
||||
this.tracks.clear();
|
||||
other.getTracks().forEach(track -> this.tracks.add(track.makeClone()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next track, i.e. the first one in the list, and removes it from the internal list.
|
||||
* @return The AudioTrack that should be played next.
|
||||
*/
|
||||
public AudioTrack getNextTrackAndRemove(boolean shouldShuffle){
|
||||
if (this.tracks.isEmpty()){
|
||||
return null;
|
||||
}
|
||||
return this.tracks.remove((shouldShuffle ? getShuffledIndex(this.tracks.size()) : 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next track to be played, and re-adds it to the end of the playlist, as it would do in a loop.
|
||||
* @return The next track to be played.
|
||||
*/
|
||||
public AudioTrack getNextTrackAndRequeue(boolean shouldShuffle){
|
||||
if (this.tracks.isEmpty()){
|
||||
return null;
|
||||
}
|
||||
AudioTrack track = this.tracks.remove((shouldShuffle ? getShuffledIndex(this.tracks.size()) : 0));
|
||||
this.tracks.add(track);
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a 'shuffled index' from a given list length. That means:
|
||||
* - A random number from 0 to (listLength-1) - threshold*(listLength), where threshold is some percentage of
|
||||
* recent songs that should be ignored; for example, the most recent 20% of the playlist can be ignored.
|
||||
* - A greater likelihood for numbers closer to 0 (those which have not been played in a while).
|
||||
* @param listLength The number of items in a potential list to choose from.
|
||||
* @return A pseudo-random choice as to which item to pick from the list.
|
||||
*/
|
||||
public static int getShuffledIndex(int listLength){
|
||||
float threshold = 0.2f;
|
||||
int trueLength = listLength - (int)threshold*listLength;
|
||||
Random rand = new Random();
|
||||
//TODO Add in a small gradient in chance for a song to be picked.
|
||||
return rand.nextInt(trueLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the playlist to a file in its name. The playlists are saved into a file in the user's home directory.
|
||||
*/
|
||||
public void save(){
|
||||
String homeDir = System.getProperty("user.home");
|
||||
File playlistDir = new File(homeDir+"/.handiebot/playlist");
|
||||
if (!playlistDir.exists()){
|
||||
if (!playlistDir.mkdirs()){
|
||||
log.log(BotLog.TYPE.ERROR, "Unable to make directory: "+playlistDir.getPath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
File playlistFile = new File(playlistDir.getPath()+"/"+this.name.replace(" ", "_")+".txt");
|
||||
log.log(BotLog.TYPE.INFO, "Saving playlist to: "+playlistFile.getAbsolutePath());
|
||||
try(Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(playlistFile)))){
|
||||
writer.write(this.name+'\n');
|
||||
writer.write(Long.toString(this.creatorUID)+'\n');
|
||||
writer.write(Integer.toString(this.tracks.size())+'\n');
|
||||
for (AudioTrack track : this.tracks){
|
||||
writer.write(track.getInfo().uri);
|
||||
writer.write('\n');
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
log.log(BotLog.TYPE.ERROR, "Unable to find file to write playlist: "+this.name);
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the playlist from a file with the playlist's name.
|
||||
*/
|
||||
public void load(AudioPlayerManager playerManager){
|
||||
String path = System.getProperty("user.home")+"/.handiebot/playlist/"+this.name.replace(" ", "_")+".txt";
|
||||
log.log(BotLog.TYPE.INFO, "Loading playlist from: "+path);
|
||||
File playlistFile = new File(path);
|
||||
if (playlistFile.exists()){
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(Paths.get(playlistFile.toURI()));
|
||||
this.name = lines.remove(0);
|
||||
this.creatorUID = Long.parseLong(lines.remove(0));
|
||||
int trackCount = Integer.parseInt(lines.remove(0));
|
||||
this.tracks = new ArrayList<>(trackCount);
|
||||
for (int i = 0; i < trackCount; i++){
|
||||
String url = lines.remove(0);
|
||||
playerManager.loadItem(url, new AudioLoadResultHandler() {
|
||||
@Override
|
||||
public void trackLoaded(AudioTrack audioTrack) {
|
||||
tracks.add(audioTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playlistLoaded(AudioPlaylist audioPlaylist) {
|
||||
//Do nothing. This should not happen.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
System.out.println("No matches for: "+url);
|
||||
//Do nothing. This should not happen.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(FriendlyException e) {
|
||||
System.out.println("Load failed: "+e.getMessage());
|
||||
//Do nothing. This should not happen.
|
||||
}
|
||||
}).get();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.log(BotLog.TYPE.ERROR, "IOException while loading playlist ["+this.name+"]. "+e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
log.log(BotLog.TYPE.ERROR, "Loading of playlist ["+this.name+"] interrupted. "+e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (ExecutionException e) {
|
||||
log.log(BotLog.TYPE.ERROR, "Execution exception while loading playlist ["+this.name+"]. "+e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,22 @@
|
|||
package handiebot.lavaplayer;
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
||||
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 handiebot.HandieBot;
|
||||
import handiebot.view.BotLog;
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IMessage;
|
||||
import sx.blah.discord.handle.obj.IVoiceChannel;
|
||||
import sx.blah.discord.util.RequestBuffer;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.List;
|
||||
|
||||
import static handiebot.HandieBot.log;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
|
@ -15,15 +24,79 @@ import java.util.concurrent.LinkedBlockingQueue;
|
|||
public class TrackScheduler extends AudioEventAdapter {
|
||||
|
||||
private final AudioPlayer player;
|
||||
private final BlockingQueue<AudioTrack> queue;
|
||||
|
||||
private Playlist activePlaylist;
|
||||
|
||||
private boolean repeat = true;
|
||||
private boolean shuffle = false;
|
||||
|
||||
private IGuild guild;
|
||||
|
||||
/**
|
||||
* Constructs a new track scheduler with the given player.
|
||||
* @param player The audio player this scheduler uses.
|
||||
*/
|
||||
public TrackScheduler(AudioPlayer player){
|
||||
public TrackScheduler(AudioPlayer player, IGuild guild, AudioPlayerManager playerManager){
|
||||
this.player = player;
|
||||
this.queue = new LinkedBlockingQueue<>();
|
||||
this.guild = guild;
|
||||
//this.activePlaylist = new Playlist("HandieBot Active Playlist", 283652989212688384L);
|
||||
this.activePlaylist = new Playlist("HandieBot Active Playlist", playerManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not songs get placed back into the queue once they're played.
|
||||
* @param value True if the playlist should repeat.
|
||||
*/
|
||||
public void setRepeat(boolean value){
|
||||
this.repeat = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not repeating is enabled.
|
||||
* @return True if repeating, false otherwise.
|
||||
*/
|
||||
public boolean isRepeating(){
|
||||
return this.repeat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not to randomize the next track to be played.
|
||||
* @param value True if shuffled should become active.
|
||||
*/
|
||||
public void setShuffle(boolean value){
|
||||
this.shuffle = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not shuffling is active.
|
||||
* @return True if shuffling is active, false otherwise.
|
||||
*/
|
||||
public boolean isShuffling(){
|
||||
return this.shuffle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time until the bot is done playing sound, at the current rate.
|
||||
* @return The milliseconds until music stops.
|
||||
*/
|
||||
public long getTimeUntilDone(){
|
||||
long t = 0;
|
||||
AudioTrack currentTrack = this.player.getPlayingTrack();
|
||||
if (currentTrack != null){
|
||||
t += currentTrack.getDuration() - currentTrack.getPosition();
|
||||
}
|
||||
for (AudioTrack track : this.queueList()){
|
||||
t += track.getDuration();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tracks in the queue.
|
||||
* @return A list of tracks in the queue.
|
||||
*/
|
||||
public List<AudioTrack> queueList(){
|
||||
return this.activePlaylist.getTracks();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,9 +104,10 @@ public class TrackScheduler extends AudioEventAdapter {
|
|||
* @param track The track to play or add to the queue.
|
||||
*/
|
||||
public void queue(AudioTrack track){
|
||||
if (!player.startTrack(track, true)){
|
||||
System.out.println("Unable to start track immediately, adding to queue.");
|
||||
queue.offer(track);
|
||||
if (player.getPlayingTrack() == null){
|
||||
player.startTrack(track, false);
|
||||
} else {
|
||||
this.activePlaylist.addTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +115,42 @@ public class TrackScheduler extends AudioEventAdapter {
|
|||
* Starts the next track, stopping the current one if it's playing.
|
||||
*/
|
||||
public void nextTrack(){
|
||||
player.startTrack(queue.poll(), false);
|
||||
AudioTrack currentTrack = this.player.getPlayingTrack();
|
||||
if (currentTrack != null){
|
||||
this.player.stopTrack();
|
||||
}
|
||||
AudioTrack track = (this.repeat ? this.activePlaylist.getNextTrackAndRequeue(this.shuffle) : this.activePlaylist.getNextTrackAndRemove(this.shuffle));
|
||||
if (track != null) {
|
||||
IVoiceChannel voiceChannel = HandieBot.musicPlayer.getVoiceChannel(this.guild);
|
||||
if (!voiceChannel.isConnected()){
|
||||
voiceChannel.join();
|
||||
}
|
||||
player.startTrack(track, false);
|
||||
} else {
|
||||
this.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user wishes to quit, stop the currently played track.
|
||||
*/
|
||||
public void quit(){
|
||||
IVoiceChannel voiceChannel = HandieBot.musicPlayer.getVoiceChannel(this.guild);
|
||||
if (voiceChannel.isConnected()){
|
||||
voiceChannel.leave();
|
||||
}
|
||||
this.player.stopTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackStart(AudioPlayer player, AudioTrack track) {
|
||||
log.log(BotLog.TYPE.MUSIC, this.guild, "Started audio track: "+track.getInfo().title);
|
||||
List<IChannel> channels = this.guild.getChannelsByName(MusicPlayer.CHANNEL_NAME.toLowerCase());
|
||||
if (channels.size() > 0){
|
||||
IMessage message = channels.get(0).sendMessage("Now playing: **"+track.getInfo().title+"**\n"+track.getInfo().uri);
|
||||
RequestBuffer.request(() -> {message.addReaction(":thumbsup:");}).get();
|
||||
RequestBuffer.request(() -> {message.addReaction(":thumbsdown:");});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,7 +158,7 @@ public class TrackScheduler extends AudioEventAdapter {
|
|||
if (endReason.mayStartNext){
|
||||
nextTrack();
|
||||
} else {
|
||||
System.out.println(endReason.toString());
|
||||
log.log(BotLog.TYPE.MUSIC, this.guild, "Unable to go to the next track. Reason: "+endReason.name());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,8 +167,4 @@ public class TrackScheduler extends AudioEventAdapter {
|
|||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) {
|
||||
super.onTrackStuck(player, track, thresholdMs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package handiebot.utils;
|
||||
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IMessage;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* Creates a message on a channel that will disappear after some time.
|
||||
*/
|
||||
public class DisappearingMessage extends Thread implements Runnable {
|
||||
|
||||
/**
|
||||
* Creates a new disappearing message that times out after some time.
|
||||
* @param channel The channel to write the message in.
|
||||
* @param message The message content.
|
||||
* @param timeout How long until the message is deleted.
|
||||
*/
|
||||
public DisappearingMessage(IChannel channel, String message, long timeout){
|
||||
IMessage sentMessage = channel.sendMessage(message);
|
||||
try {
|
||||
sleep(timeout);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
sentMessage.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a message after a set amount of time.
|
||||
* @param timeout The delay until deletion, in milliseconds.
|
||||
* @param message The message to delete.
|
||||
*/
|
||||
public static void deleteMessageAfter(long timeout, IMessage message){
|
||||
new Thread(() -> {
|
||||
try {
|
||||
sleep(timeout);
|
||||
message.delete();
|
||||
} catch (InterruptedException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package handiebot.view;
|
||||
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Style;
|
||||
import javax.swing.text.StyleConstants;
|
||||
import java.awt.*;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static handiebot.view.BotLog.TYPE.*;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class BotLog {
|
||||
|
||||
public enum TYPE {
|
||||
INFO,
|
||||
MUSIC,
|
||||
ERROR,
|
||||
COMMAND
|
||||
}
|
||||
|
||||
//Styles for output to the console.
|
||||
private Map<TYPE, Style> logStyles;
|
||||
private static Map<TYPE, Color> logStyleColors = new HashMap<TYPE, Color>(){{
|
||||
put(INFO, new Color(22, 63, 160));
|
||||
put(MUSIC, new Color(51, 175, 66));
|
||||
put(ERROR, new Color(255, 0, 0));
|
||||
put(COMMAND, new Color(255, 123, 0));
|
||||
}};
|
||||
|
||||
private Style defaultStyle;
|
||||
|
||||
private JTextPane outputArea;
|
||||
|
||||
public BotLog(JTextPane outputArea){
|
||||
this.outputArea = outputArea;
|
||||
initStyles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the styles for the various log data.
|
||||
*/
|
||||
private void initStyles(){
|
||||
this.logStyles = new HashMap<>();
|
||||
//Define default style.
|
||||
this.defaultStyle = this.outputArea.addStyle("LogStyle", null);
|
||||
this.defaultStyle.addAttribute(StyleConstants.FontFamily, "Lucida Console");
|
||||
this.defaultStyle.addAttribute(StyleConstants.FontSize, 12);
|
||||
//Define each type's color.
|
||||
for (TYPE type : TYPE.values()) {
|
||||
this.logStyles.put(type, outputArea.addStyle(type.name(), this.defaultStyle));
|
||||
this.logStyles.get(type).addAttribute(StyleConstants.Foreground, logStyleColors.get(type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the output window with the given tag and text.
|
||||
* @param type The type of message to write.
|
||||
* @param message The content of the message.
|
||||
*/
|
||||
public void log(TYPE type, String message){
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
DateFormat formatter = new SimpleDateFormat("HH:mm:ss:SSS");
|
||||
String dateFormatted = formatter.format(date);
|
||||
try {
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), dateFormatted, this.defaultStyle);
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), '['+type.name()+"] ", this.logStyles.get(type));
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), message+'\n', this.defaultStyle);
|
||||
} catch (BadLocationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the output window with the given tag, guild name, and text.
|
||||
* @param type The type of message to write.
|
||||
* @param guild The guild to get the name of.
|
||||
* @param message The content of the message.
|
||||
*/
|
||||
public void log(TYPE type, IGuild guild, String message){
|
||||
Date date = new Date(System.currentTimeMillis());
|
||||
DateFormat formatter = new SimpleDateFormat("HH:mm:ss:SSS");
|
||||
String dateFormatted = formatter.format(date);
|
||||
try {
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), dateFormatted, this.defaultStyle);
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), '['+type.name()+']', this.logStyles.get(type));
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), '['+guild.getName()+"] ", this.defaultStyle);
|
||||
this.outputArea.getStyledDocument().insertString(this.outputArea.getStyledDocument().getLength(), message+'\n', this.defaultStyle);
|
||||
} catch (BadLocationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package handiebot.view;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* This class inherits JFrame and simplifies the creation of a window.
|
||||
*/
|
||||
public class BotWindow extends JFrame {
|
||||
|
||||
public BotWindow(View view){
|
||||
super(HandieBot.APPLICATION_NAME);
|
||||
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
if (JOptionPane.showConfirmDialog((JFrame) e.getSource(), "Are you sure you want to exit and shutdown the bot?",
|
||||
"Confirm shutdown",
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION){
|
||||
HandieBot.quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
setContentPane(view.mainPanel);
|
||||
setJMenuBar(new MenuBar());
|
||||
setPreferredSize(new Dimension(800, 600));
|
||||
pack();
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package handiebot.view;
|
||||
|
||||
import handiebot.view.actions.QuitAction;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
|
||||
/**
|
||||
* Created by Andrew's Computer on 21-Jun-17.
|
||||
*/
|
||||
public class CommandLineListener implements KeyListener {
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER){
|
||||
//user wishes to submit command.
|
||||
JTextField commandLine = (JTextField) e.getSource();
|
||||
String[] words = commandLine.getText().trim().split(" ");
|
||||
commandLine.setText(null);
|
||||
String command = words[0];
|
||||
String[] args = new String[words.length-1];
|
||||
for (int i = 1; i < words.length; i++) {
|
||||
args[i-1] = words[i];
|
||||
}
|
||||
executeCommand(command, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given command on the command line.
|
||||
* @param command The first word typed, or the command itself.
|
||||
* @param args The list of arguments for the command.
|
||||
*/
|
||||
private void executeCommand(String command, String[] args){
|
||||
if (command.equals("quit")){
|
||||
new QuitAction().actionPerformed(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package handiebot.view;
|
||||
|
||||
import handiebot.view.actions.ActionItem;
|
||||
import handiebot.view.actions.QuitAction;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
* Custom menu bar to be added to the console control panel.
|
||||
*/
|
||||
public class MenuBar extends JMenuBar {
|
||||
|
||||
public MenuBar(){
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
fileMenu.add(new ActionItem("Quit", new QuitAction()));
|
||||
this.add(fileMenu);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="handiebot.view.View">
|
||||
<grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="500" height="400"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<scrollpane id="d0969">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<font name="Consolas" size="12"/>
|
||||
</properties>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="186aa" class="javax.swing.JTextPane" binding="outputArea">
|
||||
<constraints/>
|
||||
<properties>
|
||||
<editable value="false"/>
|
||||
<font name="Consolas" size="12"/>
|
||||
<selectedTextColor color="-1"/>
|
||||
<selectionColor color="-9843846"/>
|
||||
<text value=""/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</scrollpane>
|
||||
<grid id="b8806" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="f978" class="javax.swing.JTextField" binding="commandField">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
|
||||
<preferred-size width="150" height="-1"/>
|
||||
</grid>
|
||||
</constraints>
|
||||
<properties>
|
||||
<font name="DialogInput" size="16"/>
|
||||
<foreground color="-16118999"/>
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
|
@ -0,0 +1,21 @@
|
|||
package handiebot.view;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class View {
|
||||
public JPanel mainPanel;
|
||||
private JTextPane outputArea;
|
||||
private JTextField commandField;
|
||||
|
||||
public View(){
|
||||
this.commandField.addKeyListener(new CommandLineListener());
|
||||
}
|
||||
|
||||
public JTextPane getOutputArea(){
|
||||
return this.outputArea;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package handiebot.view.actions;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class ActionItem extends JMenuItem {
|
||||
|
||||
public ActionItem(String name, ActionListener listener){
|
||||
super(name);
|
||||
this.addActionListener(listener);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package handiebot.view.actions;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class QuitAction implements ActionListener {
|
||||
|
||||
private IGuild guild;
|
||||
|
||||
public QuitAction(){
|
||||
}
|
||||
|
||||
public QuitAction(IGuild guild){
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (guild != null){
|
||||
HandieBot.musicPlayer.getChatChannel(this.guild).sendMessage("Quiting HandieBot");
|
||||
HandieBot.musicPlayer.quit(this.guild);
|
||||
} else {
|
||||
HandieBot.quit();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package handiebot.view.actions.music;
|
||||
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public abstract class MusicAction implements ActionListener {
|
||||
|
||||
protected IGuild guild;
|
||||
|
||||
public MusicAction(IGuild guild) {
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package handiebot.view.actions.music;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class PlayAction extends MusicAction {
|
||||
|
||||
private String[] args = null;
|
||||
|
||||
public PlayAction(IGuild guild) {
|
||||
super(guild);
|
||||
}
|
||||
|
||||
public PlayAction(IGuild guild, String[] args){
|
||||
super(guild);
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (this.args == null || this.args.length < 1){
|
||||
HandieBot.musicPlayer.playQueue(this.guild);
|
||||
} else {
|
||||
HandieBot.musicPlayer.loadToQueue(this.guild, this.args[0]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package handiebot.view.actions.music;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class QueueListAction extends MusicAction {
|
||||
|
||||
private boolean showAll = false;
|
||||
|
||||
public QueueListAction(IGuild guild, boolean showAll){
|
||||
super(guild);
|
||||
this.showAll = showAll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
HandieBot.musicPlayer.showQueueList(this.guild, this.showAll);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package handiebot.view.actions.music;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class SkipAction extends MusicAction {
|
||||
|
||||
public SkipAction(IGuild guild) {
|
||||
super(guild);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
HandieBot.musicPlayer.skipTrack(this.guild);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package handiebot.view.actions.music;
|
||||
|
||||
import handiebot.HandieBot;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author Andrew Lalis
|
||||
*/
|
||||
public class ToggleRepeatAction extends MusicAction {
|
||||
|
||||
public ToggleRepeatAction(IGuild guild) {
|
||||
super(guild);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
HandieBot.musicPlayer.toggleRepeat(this.guild);
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 70 KiB |
|
@ -44,8 +44,8 @@
|
|||
inkscape:pageopacity="1"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.8"
|
||||
inkscape:cx="104.72807"
|
||||
inkscape:cy="108.47214"
|
||||
inkscape:cx="140.27403"
|
||||
inkscape:cy="113.84621"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
|
@ -63,7 +63,7 @@
|
|||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
@ -73,13 +73,13 @@
|
|||
id="layer1"
|
||||
transform="translate(0,-796.36219)">
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:12;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 26.071429,935.93362 C 83.571429,805.57648 182.26443,803.4602 238.57143,935.57647 226.96157,777.68558 38.855639,775.90407 26.071429,935.93362 Z"
|
||||
id="path4689"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccc" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:8.58631611;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4979"
|
||||
cx="223.82457"
|
||||
cy="925.21936"
|
||||
|
@ -92,7 +92,8 @@
|
|||
id="use4986"
|
||||
transform="translate(-181.78571,-4.4666172e-7)"
|
||||
width="100%"
|
||||
height="100%" />
|
||||
height="100%"
|
||||
style="stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<g
|
||||
id="g4981">
|
||||
<ellipse
|
||||
|
@ -101,21 +102,7 @@
|
|||
cy="936.95587"
|
||||
cx="132.5"
|
||||
id="path4691"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ff6900;stroke-width:5.30872345;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4695"
|
||||
d="m 78.571429,919.50505 c 0.357143,-21.78572 30.000001,-21.42857 30.357151,0"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:7.99999905;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<use
|
||||
height="100%"
|
||||
width="100%"
|
||||
transform="matrix(-1,0,0,1,260.35717,0.35714286)"
|
||||
id="use4977"
|
||||
xlink:href="#path4695"
|
||||
y="0"
|
||||
x="0" />
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ff6900;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.8 KiB |
Loading…
Reference in New Issue