diff --git a/interface/src/main/java/edu/regis/universeplayer/ConfigManager.java b/interface/src/main/java/edu/regis/universeplayer/ConfigManager.java index eb7fc01..636d6ec 100644 --- a/interface/src/main/java/edu/regis/universeplayer/ConfigManager.java +++ b/interface/src/main/java/edu/regis/universeplayer/ConfigManager.java @@ -9,12 +9,18 @@ import java.io.File; public class ConfigManager { + /** + * The port the player will listen for connecting instances on. + */ + public static final int PORT = 3001; + private static final Logger logger = LoggerFactory .getLogger(ConfigManager.class); private static File dataDir; private static File commDir; private static File configDir; + /** * Obtains the data storage directory for the application, creating it if * needed. diff --git a/interface/src/main/java/edu/regis/universeplayer/InstanceConnector.java b/interface/src/main/java/edu/regis/universeplayer/InstanceConnector.java new file mode 100644 index 0000000..34f6ed8 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/InstanceConnector.java @@ -0,0 +1,160 @@ +package edu.regis.universeplayer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.InputMismatchException; +import java.util.LinkedHashMap; +import java.util.Scanner; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This runner is used to handle other instances of the program wanting to + * manipulate the main state. + * + * @author William Hubbard. + * @version 0.1 + */ +public class InstanceConnector implements Runnable +{ + private static final Logger logger = + LoggerFactory.getLogger(InstanceConnector.class); + private AtomicBoolean running = new AtomicBoolean(true); + ExecutorService service = Executors.newFixedThreadPool(5); + + @Override + public void run() + { + ServerSocket server = null; + try + { + server = new ServerSocket(ConfigManager.PORT, 50, InetAddress + .getByName(null)); + + while (this.running.get()) + { + Socket socket = server.accept(); + service.submit(() -> { + try (Scanner scanner = new Scanner(socket.getInputStream())) + { + String[] args = scanner.nextLine().trim().split(" "); + ArrayList parsedArgs = new ArrayList<>(); + StringBuilder cachedString = null; + for (String arg : args) + { + if (cachedString != null) + { + if (arg.endsWith("\"")) + { + cachedString + .append(arg, 0, arg.length() - 1); + parsedArgs.add(cachedString.toString()); + cachedString = null; + } + else + { + cachedString.append(arg); + } + } + 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 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 + * before closing the stream. + */ + while (true) + { + try + { + scanner.nextInt(); + break; + } + catch (InputMismatchException e) + { + } + } + logger.debug("Finished request"); + } + catch (IOException e) + { + logger.error("Error reading socket stream"); + } + finally + { + try + { + socket.close(); + } + catch (IOException e) + { + logger.error("Could not close socket", e); + } + } + }); + } + } + catch (IOException e) + { + logger.error("Error starting server", e); + } + finally + { + if (server != null) + { + try + { + server.close(); + } + catch (IOException e) + { + logger.error("Unable to close server", e); + } + } + } + } + + /** + * Stops the server. + */ + public void stop() + { + this.running.set(false); + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java b/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java index 4c7f9b6..6b08289 100644 --- a/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java +++ b/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java @@ -3,16 +3,18 @@ package edu.regis.universeplayer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.reflect.Array; +import java.io.IOException; +import java.io.PrintStream; +import java.net.Socket; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; 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.regex.Pattern; import java.util.stream.Collectors; @@ -46,69 +48,7 @@ public class PlayerEnvironment { LinkedHashMap ops = new LinkedHashMap<>(); ArrayList params = new ArrayList<>(); - int equals = -1; - Object value; - String key; - String valStr; - int i, j, l, l2; - for (i = 0, l = args.length; i < l; i++) - { - valStr = null; - if (args[i].startsWith("-")) - { - if (args[i].startsWith("--")) - { - equals = args[i].indexOf('='); - if (equals != -1) - { - valStr = args[i].substring(equals + 1); - } - else - { - equals = args[i].length(); - valStr = null; - } - key = args[i].substring(2, equals); - } - else - { - for (j = 1, l2 = args[i].length() - 1; j < l2; j++) - { - ops.put(args[i].substring(j, j + 1), null); - } - key = args[i].substring(j, j + 1); - } - if (valStr == null && i < args.length - 1 && !args[i + 1] - .startsWith("-")) - { - valStr = args[++i]; - } - if (valStr != null) - { - if (Pattern.matches("\\d+", valStr)) - { - value = Integer.parseInt(valStr); - } - else if (Pattern.matches("\\d*\\.\\d+", valStr)) - { - value = Double.parseDouble(valStr); - } - else - { - value = valStr; - } - } - else - { - value = true; - } - ops.put(key, value); - } - else - { - params.add(args[i]); - } - } + parseArgs(args, ops, params); if (ops.containsKey("h") || ops.containsKey("help")) { @@ -116,6 +56,63 @@ public class PlayerEnvironment return; } + if (!connectToServer(args)) + { + init(ops, params); + } + } + + /** + * Forwards command arguments to the server. + * + * @param args - The argument list to forward. + * @return True if the server exists, false if not. + */ + private static boolean connectToServer(String[] args) + { + Socket socket; + try + { + socket = new Socket("localhost", ConfigManager.PORT); + try (PrintStream out = new PrintStream(socket.getOutputStream())) + { + boolean quote; + for (String arg : args) + { + 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()) + { + System.out.println(in.nextLine()); + } + } + out.print(1); + } + return true; + } + catch (IOException e) + { + logger.debug("Couldn't connect to server", e); + return false; + } + } + + private static void init(HashMap ops, + ArrayList params) + { /* * 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 @@ -123,6 +120,9 @@ public class PlayerEnvironment */ logger.info("Starting application {} {}", params, ops); + InstanceConnector connector = new InstanceConnector(); + new Thread(connector).start(); + Queue queue = Queue.getInstance(); PlayerManager playback = PlayerManager.getPlayers(); queue.addSongChangeListener(queue1 -> { @@ -146,7 +146,7 @@ public class PlayerEnvironment logger.error("Could not run command", command.getConfirmation().getError()); } - else + else if (queue1.getCurrentSong() != null) { command = PlayerManager.getPlayers() @@ -201,31 +201,258 @@ public class PlayerEnvironment /* * Open up the relevant songs */ - HashMap matchMap = new HashMap<>(); - for (String param : params) + runArguments(ops, params, System.out); + + Runtime.getRuntime().addShutdownHook(new Thread(connector::stop)); + } + + /** + * Converts an array of string arguments into a map of options and extra + * parameters. + * + * @param args - The arguments to parse. + * @param options - The map to dump the options into. + * @param params - The list to dump the other parameters into. + */ + public static void parseArgs(String[] args, + Map options, + List params) + { + int equals = -1; + Object value; + String key; + String valStr; + int i, j, l, l2; + for (i = 0, l = args.length; i < l; i++) { - String finalParam = param.toLowerCase(); - SongProvider.INSTANCE.getSongs().stream().forEach(song -> { - Integer matches = matchMap.get(song); - if (matches == null) + valStr = null; + if (args[i].startsWith("-")) + { + if (args[i].startsWith("--")) { - matches = 0; + equals = args[i].indexOf('='); + if (equals != -1) + { + valStr = args[i].substring(equals + 1); + } + else + { + equals = args[i].length(); + valStr = null; + } + key = args[i].substring(2, equals); } - if (song.title != null && song.title.toLowerCase() - .contains(finalParam)) + else { - matches++; + for (j = 1, l2 = args[i].length() - 1; j < l2; j++) + { + options.put(args[i].substring(j, j + 1), null); + } + key = args[i].substring(j, j + 1); } - if (song.album != null) + if (valStr == null && i < args.length - 1 && !args[i + 1] + .startsWith("-")) { - if (song.album.name != null && song.album.name.toLowerCase() - .contains(finalParam)) + valStr = args[++i]; + } + if (valStr != null) + { + if (Pattern.matches("\\d+", valStr)) + { + value = Integer.parseInt(valStr); + } + else if (Pattern.matches("\\d*\\.\\d+", valStr)) + { + value = Double.parseDouble(valStr); + } + else + { + value = valStr; + } + } + else + { + value = true; + } + options.put(key, value); + } + else + { + params.add(args[i]); + } + } + } + + /** + * Runs command-line arguments + * + * @param options - Run options + * @param params - A list of song terms to search for and enqueue. + * @param out - Program output. While this may simply be System .out, + * this is just as likely going to be outputting to another + * program instance. + */ + public static void runArguments(Map options, + List params, PrintStream out) + { + try + { + out.println(options.toString()); + out.println(params.toString()); + for (String op : options.keySet()) + { + switch (op) + { + case "play" -> { + PlayerManager.getPlayers().play(); + out.println("Playing"); + } + case "pause" -> { + PlayerManager.getPlayers().pause(); + out.println("Pausing"); + } + case "toggle", "t" -> { + PlayerManager.getPlayers().toggle(); + out.println("Toggling playback"); + } + case "seek" -> { + PlayerManager.getPlayers() + .seek((int) options.get("seek")); + out.println("Seeking"); + } + case "clear", "c" -> { + out.println("Clearing"); + Queue.getInstance().clear(); + } + case "next", "n" -> { + Integer number = null; + 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) + { + Queue.getInstance().skipNext(); + } + else + { + Queue.getInstance().skipToSong(Queue.getInstance() + .getCurrentIndex() + number); + } + out.println("Next Song"); + } + case "prev", "p" -> { + Integer number = null; + 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) + { + Queue.getInstance().skipNext(); + } + else + { + Queue.getInstance().skipToSong(Queue.getInstance() + .getCurrentIndex() - number); + } + out.println("Clearing"); + } + case "skip" -> { + Integer number = null; + if (options.get("skip") instanceof Integer) + { + number = (Integer) options.get("skip"); + } + if (number == null) + { + Queue.getInstance().skipNext(); + } + else + { + Queue.getInstance().skipToSong(number); + } + out.println("Clearing"); + } + case "status" -> { + QueryFuture status = + PlayerManager.getPlayers().getStatus(); + try + { + out.println(status.get()); + } + catch (InterruptedException | ExecutionException e) + { + e.printStackTrace(out); + } + } + case "song" -> { + Song song = PlayerManager.getPlayers().getCurrentSong(); + out.println(song.toString()); + } + case "queue" -> { + for (Song song : Queue.getInstance()) + { + out.print(" "); + if (song == PlayerManager.getPlayers().getCurrentSong()) + { + out.print("*"); + } + else + { + out.print(" "); + } + out.print(" "); + out.println(song.toString()); + } + } + } + } + HashMap matchMap = new HashMap<>(); + for (String param : params) + { + String finalParam = param.toLowerCase(); + SongProvider.INSTANCE.getSongs().forEach(song -> { + Integer matches = matchMap.get(song); + if (matches == null) + { + matches = 0; + } + if (song.title != null && song.title.toLowerCase() + .contains(finalParam)) { matches++; } - if (song.album.artists != null && song.album.artists.length > 0) + if (song.album != null) { - for (String artist : song.album.artists) + if (song.album.name != null && song.album.name + .toLowerCase() + .contains(finalParam)) + { + matches++; + } + if (song.album.artists != null && song.album.artists.length > 0) + { + for (String artist : song.album.artists) + { + if (artist.toLowerCase().contains(finalParam)) + { + matches++; + } + } + } + } + if (song.artists != null && song.artists.length > 0) + { + for (String artist : song.artists) { if (artist.toLowerCase().contains(finalParam)) { @@ -233,54 +460,73 @@ public class PlayerEnvironment } } } - } - if (song.artists != null && song.artists.length > 0) - { - for (String artist : song.artists) + if (matches > 0) { - if (artist.toLowerCase().contains(finalParam)) - { - matches++; - } + out.printf("%d matches for %s and %s\n", matches, + finalParam, song); + matchMap.put(song, matches); } - } - if (matches > 0) - { - matchMap.put(song, matches); - } - }); - } - if (matchMap.size() > 0) - { - List songs = matchMap.entrySet().stream().sorted(Map.Entry - .comparingByValue()).filter(entry -> (entry - .getValue() / (float) params.size()) >= 0.65F) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - System.out.println("Playing " + songs.toString()); - Collections.reverse(songs); - Queue.getInstance().addAll(songs); - } - for (String op : ops.keySet()) - { - switch (op) - { - case "play" -> PlayerManager.getPlayers().play(); - case "pause" -> PlayerManager.getPlayers().pause(); - case "toggle", "t" -> PlayerManager.getPlayers().toggle(); - case "seek" -> PlayerManager.getPlayers() - .seek((int) ops.get("seek")); + }); } + if (matchMap.size() > 0) + { + List songs = matchMap.entrySet().stream().sorted(Map.Entry + .comparingByValue()).filter(entry -> (entry + .getValue() / (float) params.size()) >= 0.65F) + .map(entry -> { + out.printf("%s: %d / %d (%f)\n", + entry.getKey(), + entry.getValue(), + params.size(), + entry.getValue() / (float) params + .size()); + return entry.getKey(); + }) + .collect(Collectors.toList()); + Collections.reverse(songs); + out.println("Playing " + songs.toString()); + Queue.getInstance().addAll(songs); + } + } + catch (Throwable e) + { + e.printStackTrace(out); } } - private static void printHelp() + public static void printHelp() { System.out.println("Universal Music Player"); System.out.println("----------------------"); System.out.println("CLI Arguments"); System.out.println("\t--headless"); System.out.println("\t\tRuns the player without a GUI."); + System.out.println("\t--play"); + System.out.println("\t\tStarts playback"); + System.out.println("\t--pause"); + System.out.println("\t\tPauses playback"); + System.out.println("\t--toggle, -t"); + System.out.println("\t\tToggles playback"); + System.out.println("\t--seek