Added better CLI support.

This approach is more flexible.
This commit is contained in:
Markil3
2021-10-31 14:05:27 -06:00
parent 0de20568a7
commit 6a1c7d86e3
4 changed files with 208 additions and 278 deletions

View File

@@ -31,6 +31,7 @@ dependencies {
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3' implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3'
implementation 'com.google.code.gson:gson:2.8.7' implementation 'com.google.code.gson:gson:2.8.7'
implementation 'commons-cli:commons-cli:1.4'
implementation 'net.harawata:appdirs:1.2.1' implementation 'net.harawata:appdirs:1.2.1'
implementation 'org.xerial:sqlite-jdbc:3.36.0.1' implementation 'org.xerial:sqlite-jdbc:3.36.0.1'
implementation 'com.googlecode.soundlibs:jlayer:1.0.1.4' implementation 'com.googlecode.soundlibs:jlayer:1.0.1.4'

View File

@@ -1,18 +1,15 @@
package edu.regis.universeplayer; package edu.regis.universeplayer;
import org.apache.commons.cli.CommandLine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.*;
import java.io.PrintStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.*;
import java.util.InputMismatchException;
import java.util.LinkedHashMap;
import java.util.Scanner;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -43,78 +40,41 @@ public class InstanceConnector implements Runnable
while (this.running.get()) while (this.running.get())
{ {
Socket socket = server.accept(); Socket socket = server.accept();
service.submit(() -> { service.submit(() ->
try (Scanner scanner = new Scanner(socket.getInputStream()))
{ {
String[] args = scanner.nextLine().trim().split(" "); try
ArrayList<String> parsedArgs = new ArrayList<>();
StringBuilder cachedString = null;
for (String arg : args)
{ {
if (cachedString != null) ObjectInputStream scanner = new ObjectInputStream(socket.getInputStream());
PrintStream stream = new PrintStream(socket.getOutputStream());
CommandLine cmd = (CommandLine) scanner.readObject();
logger.debug("Receiving commands: " + Arrays.toString(cmd.getArgs()));
if (cmd.hasOption("help"))
{ {
if (arg.endsWith("\"")) PlayerEnvironment.printHelp(stream);
{
cachedString
.append(arg, 0, arg.length() - 1);
parsedArgs.add(cachedString.toString());
cachedString = null;
} }
else else
{ {
cachedString.append(arg); PlayerEnvironment.runArguments(cmd,
stream);
} }
}
if (arg.startsWith("\""))
{
if (arg.endsWith("\""))
{
parsedArgs.add(arg.substring(1,
arg.length() - 1));
}
else
{
cachedString =
new StringBuilder(arg.substring(1));
}
}
else
{
parsedArgs.add(arg);
}
}
args = parsedArgs.toArray(String[]::new);
LinkedHashMap<String, Object> ops = new LinkedHashMap<>();
parsedArgs.clear();
PlayerEnvironment.parseArgs(args, ops, parsedArgs);
if (ops.containsKey("h") || ops.containsKey("help"))
{
PlayerEnvironment.printHelp();
return;
}
PlayerEnvironment.runArguments(ops, parsedArgs,
new PrintStream(socket.getOutputStream()));
socket.shutdownOutput();
/* /*
* Wait for the client to acknowledge the print * Wait for the client to acknowledge the print
* before closing the stream. * before closing the stream.
*/ */
while (true) stream.println();
{ stream.println("END");
try scanner.readInt();
{
scanner.nextInt();
break;
}
catch (InputMismatchException e)
{
}
}
logger.debug("Finished request"); logger.debug("Finished request");
} }
catch (IOException e) catch (EOFException e)
{ {
logger.error("Error reading socket stream"); /*
* Doesn't really matter
*/
}
catch (IOException | ClassNotFoundException e)
{
logger.error("Error reading socket stream", e);
} }
finally finally
{ {

View File

@@ -1,22 +1,12 @@
package edu.regis.universeplayer; package edu.regis.universeplayer;
import org.apache.commons.cli.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.*;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask; import java.util.concurrent.ForkJoinTask;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -24,7 +14,6 @@ import java.util.stream.Collectors;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import edu.regis.universeplayer.browserCommands.QueryFuture;
import edu.regis.universeplayer.data.AlbumProvider; import edu.regis.universeplayer.data.AlbumProvider;
import edu.regis.universeplayer.data.CompiledSongProvider; import edu.regis.universeplayer.data.CompiledSongProvider;
import edu.regis.universeplayer.data.DefaultAlbumProvider; import edu.regis.universeplayer.data.DefaultAlbumProvider;
@@ -49,6 +38,8 @@ public class PlayerEnvironment
private static final ResourceBundle langs = ResourceBundle private static final ResourceBundle langs = ResourceBundle
.getBundle("lang.interface", Locale.getDefault()); .getBundle("lang.interface", Locale.getDefault());
private static Options OPTIONS;
private static AlbumProvider ALBUMS_INSTANCE; private static AlbumProvider ALBUMS_INSTANCE;
private static SongProvider<?> SONGS_INSTANCE; private static SongProvider<?> SONGS_INSTANCE;
@@ -67,19 +58,35 @@ public class PlayerEnvironment
*/ */
public static void main(String[] args) public static void main(String[] args)
{ {
LinkedHashMap<String, Object> ops = new LinkedHashMap<>(); Options ops = setupCLIArgs();
ArrayList<String> params = new ArrayList<>(); CommandLineParser parser = new DefaultParser();
parseArgs(args, ops, params); CommandLine cmd = null;//not a good practice, it serves it purpose
if (ops.containsKey("h") || ops.containsKey("help")) try
{ {
cmd = parser.parse(ops, args);
}
catch (ParseException e)
{
System.err.println(e.getMessage());
printHelp(); printHelp();
return;
System.exit(1);
} }
if (!connectToServer(args)) if (cmd.hasOption("help"))
{ {
init(ops, params); printHelp();
System.exit(0);
}
/*
* Attempts to forward the args to the running instance, or start a new
* instance if there is no instance running.
*/
if (!connectToServer(cmd))
{
init(cmd);
} }
} }
@@ -89,57 +96,47 @@ public class PlayerEnvironment
* @param args - The argument list to forward. * @param args - The argument list to forward.
* @return True if the server exists, false if not. * @return True if the server exists, false if not.
*/ */
private static boolean connectToServer(String[] args) private static boolean connectToServer(CommandLine args)
{ {
String data;
Socket socket; Socket socket;
boolean success = false;
try try
{ {
socket = new Socket("localhost", ConfigManager.PORT); socket = new Socket("localhost", ConfigManager.PORT);
try (PrintStream out = new PrintStream(socket.getOutputStream())) ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
{ Scanner in = new Scanner(socket.getInputStream());
boolean quote; logger.debug("Connecting to remote");
for (String arg : args) out.writeObject(args);
{ logger.debug("Waiting for output");
quote = arg.contains(" ");
if (quote)
{
out.print('"');
}
out.print(arg);
if (quote)
{
out.print('"');
}
out.print(' ');
}
out.println();
try (Scanner in = new Scanner(socket.getInputStream()))
{
while (in.hasNextLine()) while (in.hasNextLine())
{ {
System.out.println(in.nextLine()); data = in.nextLine();
if (data.equals("END"))
{
break;
} }
System.out.println(data);
} }
out.print(1); success = true;
} logger.debug("Finished reading return output.");
return true; out.writeInt(-1);
} }
catch (IOException e) catch (IOException e)
{ {
logger.debug("Couldn't connect to server", e); logger.debug("Couldn't connect to server", e);
return false;
} }
return success;
} }
public static void init(Map<String, Object> ops, public static void init(CommandLine cmd)
List<String> params)
{ {
/* /*
* Add this just in case of a crash or something. It won't work if the * Add this just in case of a crash or something. It won't work if the
* program is forcibly terminated by the OS, but it could be helpful * program is forcibly terminated by the OS, but it could be helpful
* otherwise. * otherwise.
*/ */
logger.info("Starting application {} {}", params, ops); logger.info("Starting application {}", cmd);
InstanceConnector connector = new InstanceConnector(); InstanceConnector connector = new InstanceConnector();
new Thread(connector).start(); new Thread(connector).start();
@@ -151,7 +148,8 @@ public class PlayerEnvironment
Queue queue = Queue.getInstance(); Queue queue = Queue.getInstance();
PlayerManager playback = PlayerManager.getPlayers(); PlayerManager playback = PlayerManager.getPlayers();
queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() -> { queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() ->
{
ForkJoinTask<Void> command = PlayerManager.getPlayers() ForkJoinTask<Void> command = PlayerManager.getPlayers()
.stopSong(); .stopSong();
if (command != null) if (command != null)
@@ -192,7 +190,8 @@ public class PlayerEnvironment
} }
} }
})); }));
playback.addPlaybackListener(status -> { playback.addPlaybackListener(status ->
{
logger.info("Receiving {} from {}", status.getInfo(), status logger.info("Receiving {} from {}", status.getInfo(), status
.getSource()); .getSource());
if (status.getInfo().getStatus() == PlaybackStatus.FINISHED) if (status.getInfo().getStatus() == PlaybackStatus.FINISHED)
@@ -201,7 +200,7 @@ public class PlayerEnvironment
} }
}); });
if (!ops.containsKey("headless")) if (!cmd.hasOption("headless"))
{ {
Interface inter = new Interface(); Interface inter = new Interface();
inter.setSize(700, 500); inter.setSize(700, 500);
@@ -212,109 +211,103 @@ public class PlayerEnvironment
/* /*
* Open up the relevant songs * Open up the relevant songs
*/ */
runArguments(ops, params, System.out); runArguments(cmd, System.out);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() ->
{
connector.stop(); connector.stop();
PlayerManager.getPlayers().shutdownPlayers(); PlayerManager.getPlayers().shutdownPlayers();
})); }));
} }
/** private static Options setupCLIArgs()
* Converts an array of string arguments into a map of options and extra {
* parameters. if (OPTIONS == null)
* {
* @param args - The arguments to parse. OPTIONS = new Options();
* @param options - The map to dump the options into. /*
* @param params - The list to dump the other parameters into. * Launch options
*/ */
public static void parseArgs(String[] args, Option headless = Option.builder().longOpt("headless")
Map<String, Object> options, .desc("Runs the player without a GUI.")
List<String> params) .build();
{ OPTIONS.addOption(headless);
int equals; Option help = Option.builder("h").longOpt("help")
Object value; .desc("Prints this help message.")
String key; .build();
String valStr; OPTIONS.addOption(help);
int i, j, l, l2;
for (i = 0, l = args.length; i < l; i++) /*
{ * Playback options
valStr = null; */
if (args[i].startsWith("-")) Option play = Option.builder().longOpt("play")
{ .desc("Starts playback.")
if (args[i].startsWith("--")) .build();
{ OPTIONS.addOption(play);
equals = args[i].indexOf('='); Option pause = Option.builder().longOpt("pause")
if (equals != -1) .desc("Starts playback.")
{ .build();
valStr = args[i].substring(equals + 1); OPTIONS.addOption(pause);
} Option toggle = Option.builder("t").longOpt("toggle")
else .desc("Toggles playback.")
{ .build();
equals = args[i].length(); OPTIONS.addOption(toggle);
} Option seek = Option.builder().longOpt("seek")
key = args[i].substring(2, equals); .hasArg().argName("time").desc("Seeks the player to the provided time, in seconds.")
} .build();
else OPTIONS.addOption(seek);
{ Option clear = Option.builder("c").longOpt("clear")
for (j = 1, l2 = args[i].length() - 1; j < l2; j++) .desc("Clears the queue before adding songs.")
{ .build();
options.put(args[i].substring(j, j + 1), null); OPTIONS.addOption(clear);
} Option next = Option.builder("n").longOpt("next")
key = args[i].substring(j, j + 1); .hasArg().optionalArg(true).argName("skipBy")
} .desc("Skips to the next song by skipBy songs. Defaults to 1.")
if (valStr == null && i < args.length - 1 && !args[i + 1] .build();
.startsWith("-")) OPTIONS.addOption(next);
{ Option prev = Option.builder("p").longOpt("prev")
valStr = args[++i]; .hasArg().optionalArg(true).argName("skipBy")
} .desc("Skips to the previous song by skipBy songs. Defaults to 1.")
if (valStr != null) .build();
{ OPTIONS.addOption(prev);
if (Pattern.matches("\\d+", valStr)) Option skip = Option.builder().longOpt("skip")
{ .hasArg().optionalArg(true).argName("skipTo")
value = Integer.parseInt(valStr); .desc("Skips to the requested song in the queue. Defaults to the next song.")
} .build();
else if (Pattern.matches("\\d*\\.\\d+", valStr)) OPTIONS.addOption(skip);
{ Option status = Option.builder().longOpt("status")
value = Double.parseDouble(valStr); .desc("Obtains the playback status.")
} .build();
else OPTIONS.addOption(status);
{ Option queue = Option.builder().longOpt("queue")
value = valStr; .desc("Gets the queue.")
} .build();
} OPTIONS.addOption(queue);
else
{ Option search = Option.builder().longOpt("search")
value = true; .desc("Searches the library for songs matching the arguments, instead of enqueuing them.")
} .build();
options.put(key, value); OPTIONS.addOption(search);
}
else
{
params.add(args[i]);
}
} }
return OPTIONS;
} }
/** /**
* Runs command-line arguments * Runs command-line arguments
* *
* @param options - Run options * @param cmd - A list of song terms to search for and enqueue.
* @param params - A list of song terms to search for and enqueue. * @param out - Program output. While this may simply be System .out, this
* @param out - Program output. While this may simply be System .out, * is just as likely going to be outputting to another program
* this is just as likely going to be outputting to another * instance.
* program instance.
*/ */
public static void runArguments(Map<String, Object> options, public static void runArguments(CommandLine cmd, PrintStream out)
List<String> params, PrintStream out)
{ {
try try
{ {
out.println(options.toString()); out.println(cmd.toString());
out.println(params.toString()); for (Option op : cmd.getOptions())
for (String op : options.keySet())
{ {
switch (op) switch (op.getLongOpt())
{ {
case "play" -> { case "play" -> {
PlayerManager.getPlayers().play(); PlayerManager.getPlayers().play();
@@ -330,23 +323,15 @@ public class PlayerEnvironment
} }
case "seek" -> { case "seek" -> {
PlayerManager.getPlayers() PlayerManager.getPlayers()
.seek((int) options.get("seek")); .seek(Float.parseFloat(op.getValue()));
out.println("Seeking"); out.println("Seeking");
} }
case "clear", "c" -> { case "clear" -> {
out.println("Clearing"); out.println("Clearing");
Queue.getInstance().clear(); Queue.getInstance().clear();
} }
case "next", "n" -> { case "next" -> {
Integer number = null; String number = op.getValue();
if (options.get("next") instanceof Integer)
{
number = (Integer) options.get("next");
}
else if (options.get("n") instanceof Integer)
{
number = (Integer) options.get("n");
}
if (number == null) if (number == null)
{ {
Queue.getInstance().skipNext(); Queue.getInstance().skipNext();
@@ -354,20 +339,12 @@ public class PlayerEnvironment
else else
{ {
Queue.getInstance().skipToSong(Queue.getInstance() Queue.getInstance().skipToSong(Queue.getInstance()
.getCurrentIndex() + number); .getCurrentIndex() + Integer.parseInt(number));
} }
out.println("Next Song"); out.println("Next Song");
} }
case "prev", "p" -> { case "prev" -> {
Integer number = null; String number = op.getValue();
if (options.get("prev") instanceof Integer)
{
number = (Integer) options.get("prev");
}
else if (options.get("p") instanceof Integer)
{
number = (Integer) options.get("p");
}
if (number == null) if (number == null)
{ {
Queue.getInstance().skipNext(); Queue.getInstance().skipNext();
@@ -375,25 +352,21 @@ public class PlayerEnvironment
else else
{ {
Queue.getInstance().skipToSong(Queue.getInstance() Queue.getInstance().skipToSong(Queue.getInstance()
.getCurrentIndex() - number); .getCurrentIndex() - Integer.parseInt(number));
} }
out.println("Clearing"); out.println("Previous Song");
} }
case "skip" -> { case "skip" -> {
Integer number = null; String number = op.getValue();
if (options.get("skip") instanceof Integer)
{
number = (Integer) options.get("skip");
}
if (number == null) if (number == null)
{ {
Queue.getInstance().skipNext(); Queue.getInstance().skipNext();
} }
else else
{ {
Queue.getInstance().skipToSong(number); Queue.getInstance().skipToSong(Integer.parseInt(number));
} }
out.println("Clearing"); out.println("Skipping to song");
} }
case "status" -> { case "status" -> {
ForkJoinTask<PlaybackStatus> status = ForkJoinTask<PlaybackStatus> status =
@@ -423,10 +396,11 @@ public class PlayerEnvironment
} }
} }
HashMap<Song, Integer> matchMap = new HashMap<>(); HashMap<Song, Integer> matchMap = new HashMap<>();
for (String param : params) for (String param : cmd.getArgList())
{ {
String finalParam = param.toLowerCase(); String finalParam = param.toLowerCase();
getSongs().getSongs().forEach(song -> { getSongs().getSongs().forEach(song ->
{
Integer matches = matchMap.get(song); Integer matches = matchMap.get(song);
if (matches == null) if (matches == null)
{ {
@@ -476,13 +450,23 @@ public class PlayerEnvironment
{ {
List<Song> songs = matchMap.entrySet().stream().sorted(Map.Entry List<Song> songs = matchMap.entrySet().stream().sorted(Map.Entry
.comparingByValue()).filter(entry -> (entry .comparingByValue()).filter(entry -> (entry
.getValue() / (float) params.size()) >= 0.65F) .getValue() / (float) cmd.getArgList().size()) >= 0.65F)
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.reverse(songs); Collections.reverse(songs);
out.println("Playing " + songs); out.println("Playing " + songs);
if (!cmd.hasOption("search"))
{
Queue.getInstance().addAll(songs); Queue.getInstance().addAll(songs);
} }
else
{
for (Song song : songs)
{
out.println(song);
}
}
}
} }
catch (Throwable e) catch (Throwable e)
{ {
@@ -490,40 +474,24 @@ public class PlayerEnvironment
} }
} }
/**
* Prints command line arguments to the error console.
*/
public static void printHelp() public static void printHelp()
{ {
System.out.println("Universal Music Player"); printHelp(System.err);
System.out.println("----------------------"); }
System.out.println("CLI Arguments");
System.out.println("\t--headless"); /**
System.out.println("\t\tRuns the player without a GUI."); * Prints command line arguments to the provided print stream.
System.out.println("\t--play"); *
System.out.println("\t\tStarts playback"); * @param stream - The stream to print the options to.
System.out.println("\t--pause"); */
System.out.println("\t\tPauses playback"); public static void printHelp(PrintStream stream)
System.out.println("\t--toggle, -t"); {
System.out.println("\t\tToggles playback"); final HelpFormatter formatter = new HelpFormatter();
System.out.println("\t--seek <time>"); PrintWriter writer = new PrintWriter(stream);
System.out.println("\t\tSeeks the player to the provided time, in " + formatter.printHelp(formatter.getWidth(), "utility-name", "CLI Arguments", setupCLIArgs(), null);
"seconds"); writer.flush();
System.out.println("\t--clear, -c");
System.out.println("\t\tClears the queue before adding songs");
System.out.println("\t--next, -n [skipBy]");
System.out.println("\t\tSkips to the next song by skipBy songs. " +
"Defaults to 1.");
System.out.println("\t--prev, -p [skipBy]");
System.out.println("\t\tSkips to the previous song by skipBy songs. " +
"Defaults to 1.");
System.out.println("\t--skip [skipTo]");
System.out.println("\t\tSkips to the requested song in the queue. " +
"Defaults to the next song.");
System.out.println("\t--status");
System.out.println("\t\tObtains the playback status");
System.out.println("\t--song");
System.out.println("\t\tObtains the current song");
System.out.println("\t--queue");
System.out.println("\t\tGets the queue");
System.out.println("\t--help, -h");
System.out.println("\t\tPrints this help message");
} }
} }

View File

@@ -1,3 +1,4 @@
import org.apache.commons.cli.CommandLine;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@@ -28,7 +29,7 @@ public class BrowserTest
{ {
HashMap<String, Object> props = new HashMap<>(); HashMap<String, Object> props = new HashMap<>();
props.put("headless", true); props.put("headless", true);
PlayerEnvironment.init(props, Collections.emptyList()); PlayerEnvironment.init(new CommandLine.Builder().build());
} }
@Test @Test