From fb430ebf5b39cabeb6e457173ccec0a386a720eb Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 29 Jun 2017 13:29:35 +0200 Subject: [PATCH] Version 1.3.0: Many changes: * Externalized strings, so that translating phrases is much easier. There are a few log strings that must still be externalized, but all messages to discord are. * Added downvote skipping: if more than half of the people listening to a song downvote it, the song is skipped. * Added emojis to prominent commands to make it easier to understand what the bot is doing without having to read a lot. * Re-ordered execution and notification to users for some commands, so that the user has a better experience. --- pom.xml | 2 +- src/main/java/handiebot/command/Commands.java | 1 - .../handiebot/command/ReactionHandler.java | 40 ++++++++++++++++ .../commands/music/PlaylistCommand.java | 4 +- .../command/commands/music/RepeatCommand.java | 2 +- .../commands/music/ShuffleCommand.java | 2 +- .../handiebot/lavaplayer/MusicPlayer.java | 46 +++++++++++-------- .../handiebot/lavaplayer/TrackScheduler.java | 11 ++++- .../lavaplayer/playlist/Playlist.java | 3 +- .../lavaplayer/playlist/UnloadedTrack.java | 2 +- .../handiebot/utils/DisappearingMessage.java | 3 +- src/main/java/handiebot/utils/FileUtil.java | 2 +- src/main/resources/Strings.properties | 22 +++++++++ src/main/resources/Strings_nl.properties | 19 ++++++++ 14 files changed, 127 insertions(+), 32 deletions(-) diff --git a/pom.xml b/pom.xml index f47444d..7f24558 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.andrewlalis HandieBot - 1.2.0 + 1.3.0 diff --git a/src/main/java/handiebot/command/Commands.java b/src/main/java/handiebot/command/Commands.java index 2c36f9e..9a00c50 100644 --- a/src/main/java/handiebot/command/Commands.java +++ b/src/main/java/handiebot/command/Commands.java @@ -49,7 +49,6 @@ public class Commands { public static void executeCommand(String command, CommandContext context){ for (Command cmd : commands) { if (cmd.getName().equals(command)){ - System.out.println(cmd.canUserExecute(context.getUser(), context.getGuild())); if (cmd instanceof StaticCommand){ ((StaticCommand)cmd).execute(); return; diff --git a/src/main/java/handiebot/command/ReactionHandler.java b/src/main/java/handiebot/command/ReactionHandler.java index 9777180..1c3b9bc 100644 --- a/src/main/java/handiebot/command/ReactionHandler.java +++ b/src/main/java/handiebot/command/ReactionHandler.java @@ -1,6 +1,12 @@ package handiebot.command; +import handiebot.HandieBot; import sx.blah.discord.handle.impl.events.guild.channel.message.reaction.ReactionEvent; +import sx.blah.discord.handle.obj.IMessage; +import sx.blah.discord.handle.obj.IReaction; +import sx.blah.discord.handle.obj.IUser; + +import java.util.List; /** * @author Andrew Lalis @@ -8,12 +14,46 @@ import sx.blah.discord.handle.impl.events.guild.channel.message.reaction.Reactio */ public class ReactionHandler { + public static final String thumbsUp = "\uD83D\uDC4D"; + public static final String thumbsDown = "\uD83D\uDC4E"; + /** * Processes a reaction. * @param event The reaction event to process. */ public static void handleReaction(ReactionEvent event){ + IMessage message = event.getMessage(); + IReaction reaction = event.getReaction(); + CommandContext context = new CommandContext(event.getUser(), event.getChannel(), event.getGuild(), new String[]{}); + if (reaction.toString().equals(thumbsDown)){ + onDownvote(context, message); + } + } + /** + * What to do if someone downvotes a song. + * If more than half of the people in the voice channel dislike the song, it will be skipped. + * If not, then the bot will tell how many more people need to downvote. + * @param context The context of the reaction. + * @param message The messages that received a reaction. + */ + private static void onDownvote(CommandContext context, IMessage message){ + List usersHere = HandieBot.musicPlayer.getVoiceChannel(context.getGuild()).getConnectedUsers(); + usersHere.removeIf(user -> user.getLongID() == HandieBot.client.getOurUser().getLongID()); + int userCount = usersHere.size(); + int userDownvotes = 0; + IReaction reaction = message.getReactionByUnicode(thumbsDown); + for (IUser user : reaction.getUsers()){ + if (usersHere.contains(user)){ + userDownvotes++; + } + } + System.out.println("Valid downvotes: "+userDownvotes+" out of "+userCount+" people present."); + if (userDownvotes > (userCount/2)){ + HandieBot.musicPlayer.skipTrack(context.getGuild()); + } else if (userDownvotes > 0) { + context.getChannel().sendMessage((((userCount/2)+1) - userDownvotes)+" more people must downvote before the track is skipped."); + } } } diff --git a/src/main/java/handiebot/command/commands/music/PlaylistCommand.java b/src/main/java/handiebot/command/commands/music/PlaylistCommand.java index da63f34..d61fa02 100644 --- a/src/main/java/handiebot/command/commands/music/PlaylistCommand.java +++ b/src/main/java/handiebot/command/commands/music/PlaylistCommand.java @@ -183,10 +183,10 @@ public class PlaylistCommand extends ContextCommand { return; Playlist playlist = new Playlist(context.getArgs()[1]); playlist.load(); - HandieBot.musicPlayer.getMusicManager(context.getGuild()).scheduler.setPlaylist(playlist); - HandieBot.musicPlayer.getMusicManager(context.getGuild()).scheduler.nextTrack(); log.log(BotLog.TYPE.INFO, MessageFormat.format(resourceBundle.getString("commands.command.playlist.play.log"), playlist.getName())); context.getChannel().sendMessage(MessageFormat.format(resourceBundle.getString("commands.command.playlist.play.message"), playlist.getName())); + HandieBot.musicPlayer.getMusicManager(context.getGuild()).scheduler.setPlaylist(playlist); + HandieBot.musicPlayer.getMusicManager(context.getGuild()).scheduler.nextTrack(); } else { context.getChannel().sendMessage(MessageFormat.format(resourceBundle.getString("commands.command.playlist.error.playPlaylistNeeded"), getPlaylistShowString(context))); } diff --git a/src/main/java/handiebot/command/commands/music/RepeatCommand.java b/src/main/java/handiebot/command/commands/music/RepeatCommand.java index f2b5fd4..864b1f5 100644 --- a/src/main/java/handiebot/command/commands/music/RepeatCommand.java +++ b/src/main/java/handiebot/command/commands/music/RepeatCommand.java @@ -11,7 +11,7 @@ import static handiebot.HandieBot.resourceBundle; * Command to toggle repeating of the active playlist. */ public class RepeatCommand extends ContextCommand { - +//TODO: make changing settings admin-only public RepeatCommand(){ super("repeat", "[true|false]", diff --git a/src/main/java/handiebot/command/commands/music/ShuffleCommand.java b/src/main/java/handiebot/command/commands/music/ShuffleCommand.java index 81c7825..3cc1304 100644 --- a/src/main/java/handiebot/command/commands/music/ShuffleCommand.java +++ b/src/main/java/handiebot/command/commands/music/ShuffleCommand.java @@ -11,7 +11,7 @@ import static handiebot.HandieBot.resourceBundle; * Command to set shuffling of the active playlist. */ public class ShuffleCommand extends ContextCommand { - +//TODO: make changes admin-only public ShuffleCommand(){ super("shuffle", "[true|false]", diff --git a/src/main/java/handiebot/lavaplayer/MusicPlayer.java b/src/main/java/handiebot/lavaplayer/MusicPlayer.java index 5115816..502658d 100644 --- a/src/main/java/handiebot/lavaplayer/MusicPlayer.java +++ b/src/main/java/handiebot/lavaplayer/MusicPlayer.java @@ -3,7 +3,6 @@ package handiebot.lavaplayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; -import handiebot.HandieBot; import handiebot.command.Commands; import handiebot.lavaplayer.playlist.Playlist; import handiebot.lavaplayer.playlist.UnloadedTrack; @@ -15,12 +14,14 @@ import sx.blah.discord.handle.obj.IGuild; import sx.blah.discord.handle.obj.IVoiceChannel; import sx.blah.discord.util.EmbedBuilder; +import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static handiebot.HandieBot.log; +import static handiebot.HandieBot.resourceBundle; /** * @author Andrew Lalis @@ -65,7 +66,6 @@ public class MusicPlayer { */ public 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()); } @@ -82,7 +82,7 @@ public class MusicPlayer { if (!this.chatChannels.containsKey(guild)){ List channels = guild.getChannelsByName(CHANNEL_NAME.toLowerCase()); if (channels.isEmpty()){ - log.log(BotLog.TYPE.MUSIC, guild, "No chat channel found, creating a new one."); + log.log(BotLog.TYPE.MUSIC, guild, resourceBundle.getString("log.creatingChatChannel")); this.chatChannels.put(guild, guild.createChannel(CHANNEL_NAME.toLowerCase())); } else { this.chatChannels.put(guild, channels.get(0)); @@ -101,7 +101,7 @@ public class MusicPlayer { if (!this.voiceChannels.containsKey(guild)){ List channels = guild.getVoiceChannelsByName(CHANNEL_NAME); if (channels.isEmpty()){ - log.log(BotLog.TYPE.MUSIC, guild, "No voice channel found, creating a new one."); + log.log(BotLog.TYPE.MUSIC, guild, resourceBundle.getString("log.newVoiceChannel")); this.voiceChannels.put(guild, guild.createVoiceChannel(CHANNEL_NAME)); } else { this.voiceChannels.put(guild, channels.get(0)); @@ -125,8 +125,9 @@ public class MusicPlayer { */ public void setRepeat(IGuild guild, boolean value){ getMusicManager(guild).scheduler.setRepeat(value); - log.log(BotLog.TYPE.MUSIC, guild, "Set repeat to "+getMusicManager(guild).scheduler.isRepeating()); - getChatChannel(guild).sendMessage("Set repeat to "+getMusicManager(guild).scheduler.isRepeating()); + String message = MessageFormat.format(resourceBundle.getString("player.setRepeat"), getMusicManager(guild).scheduler.isRepeating()); + log.log(BotLog.TYPE.MUSIC, guild, message); + getChatChannel(guild).sendMessage(":repeat: "+message); } /** @@ -144,8 +145,9 @@ public class MusicPlayer { */ public void setShuffle(IGuild guild, boolean value){ getMusicManager(guild).scheduler.setShuffle(value); - log.log(BotLog.TYPE.MUSIC, guild, "Set shuffle to "+Boolean.toString(HandieBot.musicPlayer.getMusicManager(guild).scheduler.isShuffling())); - getChatChannel(guild).sendMessage("Set shuffle to "+Boolean.toString(HandieBot.musicPlayer.getMusicManager(guild).scheduler.isShuffling())); + String message = MessageFormat.format(resourceBundle.getString("player.setShuffle"), getMusicManager(guild).scheduler.isShuffling()); + log.log(BotLog.TYPE.MUSIC, guild, message); + getChatChannel(guild).sendMessage(":twisted_rightwards_arrows: "+message); } /** @@ -154,16 +156,16 @@ public class MusicPlayer { public void showQueueList(IGuild guild, boolean showAll) { List tracks = getMusicManager(guild).scheduler.queueList(); if (tracks.size() == 0) { - getChatChannel(guild).sendMessage("The queue is empty. Use `"+ Commands.get("play").getUsage()+"` to add songs."); + getChatChannel(guild).sendMessage(MessageFormat.format(resourceBundle.getString("player.queueEmpty"), Commands.get("play").getUsage())); } else { if (tracks.size() > 10 && showAll) { String result = Pastebin.paste("Current queue for discord server: "+guild.getName()+".", getMusicManager(guild).scheduler.getActivePlaylist().toString()); if (result != null && result.startsWith("https://pastebin.com/")){ - log.log(BotLog.TYPE.INFO, guild, "Queue uploaded to pastebin: "+result); + log.log(BotLog.TYPE.INFO, guild, MessageFormat.format(resourceBundle.getString("player.queueUploaded"), result)); //Only display the pastebin link for 10 minutes. - new DisappearingMessage(getChatChannel(guild), "You may view the full queue by following the link: "+result+"\nNote that this link expires in 10 minutes.", 600000); + new DisappearingMessage(getChatChannel(guild), MessageFormat.format(resourceBundle.getString("player.pastebinLink"), result), 600000); } else { - log.log(BotLog.TYPE.ERROR, guild, "Unable to upload to pastebin: "+result); + log.log(BotLog.TYPE.ERROR, guild, MessageFormat.format(resourceBundle.getString("player.pastebinError"), result)); } } else { EmbedBuilder builder = new EmbedBuilder(); @@ -174,7 +176,7 @@ public class MusicPlayer { sb.append(tracks.get(i).getURL()).append(")"); sb.append(tracks.get(i).getFormattedDuration()).append('\n'); } - builder.appendField("Showing " + (tracks.size() <= 10 ? tracks.size() : "the first 10") + " track" + (tracks.size() > 1 ? "s" : "") + " out of "+tracks.size()+".", sb.toString(), false); + builder.appendField(MessageFormat.format(resourceBundle.getString("player.queueHeader"), tracks.size() <= 10 ? tracks.size() : "the first 10", tracks.size() > 1 ? "s" : "", tracks.size()), sb.toString(), false); getChatChannel(guild).sendMessage(builder.build()); } } @@ -195,7 +197,7 @@ public class MusicPlayer { //Build message. StringBuilder sb = new StringBuilder(); if (timeUntilPlay > 0) { - sb.append("Added **").append(track.getTitle()).append("** to the queue."); + sb.append(MessageFormat.format(resourceBundle.getString("player.addedToQueue"), track.getTitle())); } //If there's some tracks in the queue, get the time until this one plays. if (timeUntilPlay > 0){ @@ -215,6 +217,10 @@ public class MusicPlayer { * If possible, try to begin playing from the track scheduler's queue. */ public void playQueue(IGuild guild){ + if (getMusicManager(guild).scheduler.getActivePlaylist().getTrackCount() == 0){ + getChatChannel(guild).sendMessage(resourceBundle.getString("player.playQueueEmpty")); + return; + } IVoiceChannel vc = this.getVoiceChannel(guild); if (!vc.isConnected()){ vc.join(); @@ -224,16 +230,17 @@ public class MusicPlayer { public void clearQueue(IGuild guild){ getMusicManager(guild).scheduler.clearQueue(); - getChatChannel(guild).sendMessage("Cleared the queue."); + getChatChannel(guild).sendMessage(resourceBundle.getString("player.queueCleared")); } /** * Skips the current track. */ public void skipTrack(IGuild guild){ + String message = resourceBundle.getString("player.skippingCurrent"); + log.log(BotLog.TYPE.MUSIC, guild, message); + getChatChannel(guild).sendMessage(":track_next: "+message); getMusicManager(guild).scheduler.nextTrack(); - log.log(BotLog.TYPE.MUSIC, guild, "Skipping the current track. "); - getChatChannel(guild).sendMessage("Skipping the current track."); } /** @@ -242,8 +249,9 @@ public class MusicPlayer { */ public void stop(IGuild guild){ getMusicManager(guild).scheduler.stop(); - getChatChannel(guild).sendMessage("Stopped playing music."); - log.log(BotLog.TYPE.MUSIC, guild, "Stopped playing music."); + String message = resourceBundle.getString("player.musicStopped"); + getChatChannel(guild).sendMessage(":stop_button: "+message); + log.log(BotLog.TYPE.MUSIC, guild, message); } /** diff --git a/src/main/java/handiebot/lavaplayer/TrackScheduler.java b/src/main/java/handiebot/lavaplayer/TrackScheduler.java index 525699b..39cbd2d 100644 --- a/src/main/java/handiebot/lavaplayer/TrackScheduler.java +++ b/src/main/java/handiebot/lavaplayer/TrackScheduler.java @@ -15,12 +15,19 @@ import sx.blah.discord.handle.obj.IMessage; import sx.blah.discord.handle.obj.IVoiceChannel; import sx.blah.discord.util.RequestBuffer; +import java.text.MessageFormat; import java.util.List; import static handiebot.HandieBot.log; +import static handiebot.HandieBot.resourceBundle; /** * @author Andrew Lalis + * Class to actually play music. + *

+ * It holds an active playlist which it uses to pull songs from, and through the {@code MusicPlayer}, the + * playlist can be modified. + *

*/ public class TrackScheduler extends AudioEventAdapter { @@ -180,10 +187,10 @@ public class TrackScheduler extends AudioEventAdapter { @Override public void onTrackStart(AudioPlayer player, AudioTrack track) { - log.log(BotLog.TYPE.MUSIC, this.guild, "Started audio track: "+track.getInfo().title); + log.log(BotLog.TYPE.MUSIC, this.guild, MessageFormat.format(resourceBundle.getString("trackSchedule.trackStarted"), track.getInfo().title)); List channels = this.guild.getChannelsByName(MusicPlayer.CHANNEL_NAME.toLowerCase()); if (channels.size() > 0){ - IMessage message = channels.get(0).sendMessage("Now playing: **"+track.getInfo().title+"** "+new UnloadedTrack(track).getFormattedDuration()+"\n"+track.getInfo().uri); + IMessage message = channels.get(0).sendMessage(MessageFormat.format(":arrow_forward: "+resourceBundle.getString("trackSchedule.nowPlaying"), track.getInfo().title, new UnloadedTrack(track).getFormattedDuration())); RequestBuffer.request(() -> {message.addReaction(":thumbsup:");}).get(); RequestBuffer.request(() -> {message.addReaction(":thumbsdown:");}).get(); } diff --git a/src/main/java/handiebot/lavaplayer/playlist/Playlist.java b/src/main/java/handiebot/lavaplayer/playlist/Playlist.java index 538f911..a9be6b3 100644 --- a/src/main/java/handiebot/lavaplayer/playlist/Playlist.java +++ b/src/main/java/handiebot/lavaplayer/playlist/Playlist.java @@ -21,7 +21,7 @@ import static handiebot.HandieBot.log; * on the playlist. */ public class Playlist { - +//TODO: externalize strings private String name; private List tracks; @@ -121,7 +121,6 @@ public class Playlist { public static int getShuffledIndex(int listLength){ float threshold = 0.2f; int trueLength = listLength - (int)(threshold*(float)listLength); - log.log(BotLog.TYPE.INFO, "Shuffle results: Actual size: "+listLength+", Last Usable Index: "+trueLength); Random rand = new Random(); //TODO Add in a small gradient in chance for a song to be picked. return rand.nextInt(trueLength); diff --git a/src/main/java/handiebot/lavaplayer/playlist/UnloadedTrack.java b/src/main/java/handiebot/lavaplayer/playlist/UnloadedTrack.java index e2315c1..ed758f8 100644 --- a/src/main/java/handiebot/lavaplayer/playlist/UnloadedTrack.java +++ b/src/main/java/handiebot/lavaplayer/playlist/UnloadedTrack.java @@ -17,7 +17,7 @@ import static handiebot.HandieBot.log; * This is useful for quickly loading playlists and only loading a track when it is needed. */ public class UnloadedTrack implements Cloneable { - +//TODO: externalize strings private String title; private String url; private long duration; diff --git a/src/main/java/handiebot/utils/DisappearingMessage.java b/src/main/java/handiebot/utils/DisappearingMessage.java index f9b18aa..82aec7a 100644 --- a/src/main/java/handiebot/utils/DisappearingMessage.java +++ b/src/main/java/handiebot/utils/DisappearingMessage.java @@ -7,6 +7,7 @@ import sx.blah.discord.handle.obj.IMessage; import sx.blah.discord.handle.obj.Permissions; import static handiebot.HandieBot.log; +import static handiebot.HandieBot.resourceBundle; /** * @author Andrew Lalis @@ -57,7 +58,7 @@ public class DisappearingMessage extends Thread implements Runnable { if (HandieBot.hasPermission(Permissions.MANAGE_MESSAGES, message.getChannel())){ return true; } else { - log.log(BotLog.TYPE.ERROR, message.getGuild(), "Unable to delete message. Please ensure that the bot has MANAGE_MESSAGES enabled, especially for this channel."); + log.log(BotLog.TYPE.ERROR, message.getGuild(), resourceBundle.getString("log.deleteMessageError")); return false; } } diff --git a/src/main/java/handiebot/utils/FileUtil.java b/src/main/java/handiebot/utils/FileUtil.java index 5a01126..26749bd 100644 --- a/src/main/java/handiebot/utils/FileUtil.java +++ b/src/main/java/handiebot/utils/FileUtil.java @@ -17,7 +17,7 @@ import static handiebot.HandieBot.log; * Class to simplify file operations. */ public class FileUtil { - +//TODO: externalize strings public static String getDataDirectory(){ return System.getProperty("user.home")+"/.handiebot/"; } diff --git a/src/main/resources/Strings.properties b/src/main/resources/Strings.properties index 5610cd0..d77710d 100644 --- a/src/main/resources/Strings.properties +++ b/src/main/resources/Strings.properties @@ -1,7 +1,12 @@ +#Strings for HandieBot: +# The following strings are organized in a way that it should be intuitive how it will be used. #Log log.loggingIn=Logging client in... log.init=HandieBot initialized. log.shuttingDown=Shutting down the bot. +log.deleteMessageError=Unable to delete message. Please ensure that the bot has MANAGE_MESSAGES enabled, especially for this channel. +log.creatingChatChannel=No chat channel found, creating a new one. +log.newVoiceChannel=No voice channel found, creating a new one. #Window window.close.question=Are you sure you want to exit and shutdown the bot? window.close.title=Confirm shutdown @@ -85,3 +90,20 @@ commands.command.shuffle.description=Sets shuffling. commands.command.skip.description=Skips the current song. #Stop commands.command.stop.description=Stops playing music. +#Music Player +player.setRepeat=Set repeat to {0} +player.setShuffle=Set shuffle to {0} +player.queueEmpty=The queue is empty. Use `{0}` to add songs. +player.queueUploaded=Queue uploaded to pastebin: {0} +player.pastebinLink=You may view the full queue by following the link: {0}\nNote that this link expires in 10 minutes. +player.pastebinError=Unable to upload to pastebin: {0} +player.queueHeader=Showing {0} track{1} out of {2}. +player.addedToQueue=Added **{0}** to the queue. +player.queueCleared=Cleared the queue. +player.skippingCurrent=Skipping the current track. +player.musicStopped=Stopped playing music. +player.playQueueEmpty=There's nothing in the queue to play. +#Track scheduler +trackSchedule.trackStarted=Started audio track: {0} +trackSchedule.nowPlaying=Now playing: **{0}** {1} + diff --git a/src/main/resources/Strings_nl.properties b/src/main/resources/Strings_nl.properties index 00d8375..b26e43d 100644 --- a/src/main/resources/Strings_nl.properties +++ b/src/main/resources/Strings_nl.properties @@ -80,4 +80,23 @@ commands.command.repeat.description=Sets repeating. commands.command.shuffle.description=Sets shuffling. commands.command.skip.description=Skips the current song. commands.command.stop.description=Stops playing music. +log.deleteMessageError=Unable to delete message. Please ensure that the bot has MANAGE_MESSAGES enabled, especially for this channel. +log.creatingChatChannel=No chat channel found, creating a new one. +log.newVoiceChannel=No voice channel found, creating a new one. +player.setRepeat=Set repeat to {0} +player.setShuffle=Set shuffle to {0} +player.queueEmpty=The queue is empty. Use `{0}` to add songs. +player.queueUploaded=Queue uploaded to pastebin: {0} +player.pastebinLink=You may view the full queue by following the link: {0}\ +Note that this link expires in 10 minutes. +player.pastebinError=Unable to upload to pastebin: {0} +player.queueHeader=Showing {0} track{1} out of {2}. +player.addedToQueue=Added **{0}** to the queue. +player.queueCleared=Cleared the queue. +player.skippingCurrent=Skipping the current track. +player.musicStopped=Stopped playing music. +trackSchedule.trackStarted=Started audio track: {0} +trackSchedule.nowPlaying=Now playing: **{0}** {1}\ +{2} +player.playQueueEmpty=There's nothing in the queue to play.