diff --git a/src/main/java/land/chipmunk/chipmunkbot/command/arguments/LocationArgumentType.java b/src/main/java/land/chipmunk/chipmunkbot/command/arguments/LocationArgumentType.java new file mode 100644 index 0000000..e6578b2 --- /dev/null +++ b/src/main/java/land/chipmunk/chipmunkbot/command/arguments/LocationArgumentType.java @@ -0,0 +1,88 @@ +package land.chipmunk.chipmunkbot.command.arguments; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.kyori.adventure.text.Component; +import land.chipmunk.chipmunkbot.command.ComponentMessage; +import java.util.Collection; +import java.util.Arrays; +import java.net.URL; +import java.net.MalformedURLException; +import java.nio.file.Path; + +public class LocationArgumentType implements ArgumentType { + private static final Collection EXAMPLES = Arrays.asList("songs/amogus.mid", "images/cat.jpg", "videos/badapple.mp4"); + + private static final SimpleCommandExceptionType OOB_FILEPATH = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("The specified file path is outside of the allowed directory"))); + + private boolean allowsUrls = false; + private boolean allowsPaths = false; + private Path root; + + private LocationArgumentType (boolean allowsUrls, boolean allowsPaths, Path root) { + this.allowsUrls = allowsUrls; + this.allowsPaths = allowsPaths; + this.root = root.toAbsolutePath().normalize(); + } + + public static LocationArgumentType location (Path rootPath) { return new LocationArgumentType(true, true, rootPath); } + public static LocationArgumentType url () { return new LocationArgumentType(true, false, null); } + public static LocationArgumentType filepath (Path rootPath) { return new LocationArgumentType(false, true, rootPath); } + + @Override + public Object parse (StringReader reader) throws CommandSyntaxException { + final String remaining = reader.getString().substring(reader.getCursor()); + if (allowsUrls && isUrlStart(remaining)) return parseUrl(reader); + if (allowsPaths) return parsePath(reader); + return null; + } + + public boolean isUrlStart (String string) { return string.startsWith("http://") || string.startsWith("https://") || string.startsWith("ftp://"); } + + public URL parseUrl (StringReader reader) throws CommandSyntaxException { + final StringBuilder sb = new StringBuilder(); + while (reader.canRead() && reader.peek() != ' ') { + sb.append(reader.read()); + } + + try { + return new URL(sb.toString()); + } catch (MalformedURLException exception) { + throw new SimpleCommandExceptionType(ComponentMessage.wrap(Component.text(exception.getMessage()))).create(); + } + } + + public Path parsePath (StringReader reader) throws CommandSyntaxException { + final String pathString = reader.readString(); + final Path path = Path.of(root.toString(), pathString).toAbsolutePath().normalize(); + if (!path.startsWith(root)) throw OOB_FILEPATH.create(); + return path; + } + + private static Object getLocation (CommandContext context, String name) { + return context.getArgument(name, Object.class); + } + + public static URL getUrl (CommandContext context, String name) { + final Object location = getLocation(context, name); + if (location instanceof URL) return (URL) location; + try { + if (location instanceof Path) return new URL("file", "", -1, location.toString()); + } catch (MalformedURLException ignored) { + return null; // The real question is whether this will actually ever get called + } + return null; + } + + public static Path getPath (CommandContext context, String name) { + final Object location = getLocation(context, name); + if (location instanceof Path) return (Path) location; + return null; + } + + @Override + public Collection getExamples () { return EXAMPLES; } +} diff --git a/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java b/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java index 315f637..0e64fcf 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java +++ b/src/main/java/land/chipmunk/chipmunkbot/commands/MusicCommand.java @@ -9,6 +9,10 @@ import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; import static com.mojang.brigadier.arguments.StringArgumentType.getString; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; +import static land.chipmunk.chipmunkbot.command.arguments.LocationArgumentType.location; +import static land.chipmunk.chipmunkbot.command.arguments.LocationArgumentType.filepath; +import static land.chipmunk.chipmunkbot.command.arguments.LocationArgumentType.getPath; +import static land.chipmunk.chipmunkbot.command.arguments.LocationArgumentType.getUrl; import static land.chipmunk.chipmunkbot.command.arguments.TimestampArgumentType.timestamp; import static com.mojang.brigadier.arguments.LongArgumentType.getLong; import com.mojang.brigadier.context.CommandContext; @@ -17,22 +21,26 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.JoinConfiguration; +import java.nio.file.Path; import java.util.List; import java.util.ArrayList; public class MusicCommand extends Command { private static SimpleCommandExceptionType NO_SONG_IS_CURRENTLY_PLAYING = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("No song is currently playing"))); private static SimpleCommandExceptionType OOB_TIMESTAMP = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("Invalid timestamp for the current song"))); + private static SimpleCommandExceptionType DIRECTORY_DOES_NOT_EXIST = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("The specified directory does not exist"))); public MusicCommand () { super(); + Path root = Path.of(SongPlayer.SONG_DIR.getPath()); + this.node( literal("music") .then( literal("play") .then( - argument("location", greedyString()) + argument("location", location(root)) .executes(this::play) ) ) @@ -40,7 +48,15 @@ public class MusicCommand extends Command { .then(literal("stop").executes(this::stop)) .then(literal("skip").executes(this::skip)) .then(literal("pause").executes(this::pause)) - .then(literal("list").executes(this::list)) + + .then( + literal("list") + .executes(c -> list(c, root)) + .then( + argument("location", filepath(root)) + .executes(c -> list(c, getPath(c, "location"))) + ) + ) .then( literal("loop") @@ -62,8 +78,12 @@ public class MusicCommand extends Command { } public int play (CommandContext context) { - final String location = getString(context, "location"); - context.getSource().client().songPlayer().loadSong(location); + final SongPlayer songPlayer = context.getSource().client().songPlayer(); + + final Path path = getPath(context, "location"); + + if (path != null) songPlayer.loadSong(path); + else songPlayer.loadSong(getUrl(context, "location")); return 1; } @@ -111,13 +131,16 @@ public class MusicCommand extends Command { return 1; } - public int list (CommandContext context) { + public int list (CommandContext context, Path path) throws CommandSyntaxException { final CommandSource source = context.getSource(); final SongPlayer songPlayer = source.client().songPlayer(); + final String[] filenames = path.toFile().list(); + if (filenames == null) throw DIRECTORY_DOES_NOT_EXIST.create(); + final List list = new ArrayList<>(); int i = 0; - for (String filename : songPlayer.SONG_DIR.list()) { + for (String filename : filenames) { list.add(Component.text(filename, (i++ & 1) == 0 ? NamedTextColor.DARK_GREEN : NamedTextColor.GREEN)); } diff --git a/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java b/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java index 1978201..5508c38 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java +++ b/src/main/java/land/chipmunk/chipmunkbot/plugins/SongPlayer.java @@ -13,6 +13,8 @@ import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import lombok.Getter; import lombok.Setter; import java.io.File; +import java.nio.file.Path; +import java.net.URL; import java.util.Timer; import java.util.TimerTask; import java.util.LinkedList; @@ -42,7 +44,9 @@ public class SongPlayer extends SessionAdapter { }); } - public void loadSong (String location) { + // TODO: Less duplicate code + + public void loadSong (Path location) { if (loaderThread != null) { client.chat().tellraw(Component.translatable("Already loading a song, cannot load another", NamedTextColor.RED)); return; @@ -50,7 +54,24 @@ public class SongPlayer extends SessionAdapter { try { final SongLoaderThread _loaderThread = new SongLoaderThread(location); - client.chat().tellraw(Component.translatable("Loading %s", Component.text(location, NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); + client.chat().tellraw(Component.translatable("Loading %s", Component.text(location.getFileName().toString(), NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); + _loaderThread.start(); + loaderThread = _loaderThread; + } catch (SongLoaderException e) { + client.chat().tellraw(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED)); + loaderThread = null; + } + } + +public void loadSong (URL location) { + if (loaderThread != null) { + client.chat().tellraw(Component.translatable("Already loading a song, cannot load another", NamedTextColor.RED)); + return; + } + + try { + final SongLoaderThread _loaderThread = new SongLoaderThread(location); + client.chat().tellraw(Component.translatable("Loading %s", Component.text(location.toString(), NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN)); _loaderThread.start(); loaderThread = _loaderThread; } catch (SongLoaderException e) { diff --git a/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java index f2183c2..bfe48a0 100644 --- a/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java +++ b/src/main/java/land/chipmunk/chipmunkbot/song/SongLoaderThread.java @@ -6,6 +6,7 @@ import net.kyori.adventure.text.Component; import java.io.File; import java.io.IOException; import java.net.URL; +import java.nio.file.Path; import java.nio.file.Files; import java.nio.file.Paths; @@ -18,30 +19,16 @@ public class SongLoaderThread extends Thread { private boolean isUrl = false; - public SongLoaderThread (String location) throws SongLoaderException { - this.location = location; - if (location.startsWith("http://") || location.startsWith("https://")) { - isUrl = true; - try { - songUrl = new URL(location); - } catch (IOException exception) { - throw new SongLoaderException(Component.text(exception.getMessage()), exception); - } - } else if (location.contains("/") || location.contains("\\")) { - throw new SongLoaderException(Component.translatable("Invalid characters in song name: %s", Component.text(location))); - } else if (getSongFile(location).exists()) { - songPath = getSongFile(location); - } else if (getSongFile(location+".mid").exists()) { - songPath = getSongFile(location+".mid"); - } else if (getSongFile(location+".midi").exists()) { - songPath = getSongFile(location+".midi"); - } else if (getSongFile(location+".nbs").exists()) { - songPath = getSongFile(location+".nbs"); - } else { - throw new SongLoaderException(Component.translatable("Could not find song: %s", Component.text(location))); - } + public SongLoaderThread (URL location) throws SongLoaderException { + isUrl = true; + songUrl = location; } - + + public SongLoaderThread (Path location) throws SongLoaderException { + isUrl = false; + songPath = location.toFile(); + } + public void run () { byte[] bytes; String name;