From 699150c5d888e6bf146890aa2f8627b4b96bfde4 Mon Sep 17 00:00:00 2001 From: LabyStudio Date: Mon, 8 Jan 2024 15:50:28 +0100 Subject: [PATCH] improve dbus & mpris api (wip) --- .../platform/linux/LinuxSpotifyApi.java | 23 ++-- ...{MetadataParser.java => DBusResponse.java} | 55 ++++----- .../platform/linux/api/DBusSend.java | 108 ++++++++++++++++++ .../platform/linux/api/InterfaceMember.java | 20 ++++ .../platform/linux/api/MPRISCommunicator.java | 94 ++++++--------- .../platform/linux/api/Parameter.java | 34 ++++++ 6 files changed, 240 insertions(+), 94 deletions(-) rename src/main/java/de/labystudio/spotifyapi/platform/linux/api/{MetadataParser.java => DBusResponse.java} (59%) create mode 100644 src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusSend.java create mode 100644 src/main/java/de/labystudio/spotifyapi/platform/linux/api/InterfaceMember.java create mode 100644 src/main/java/de/labystudio/spotifyapi/platform/linux/api/Parameter.java diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/LinuxSpotifyApi.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/LinuxSpotifyApi.java index 9789e2e..01f34b7 100644 --- a/src/main/java/de/labystudio/spotifyapi/platform/linux/LinuxSpotifyApi.java +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/LinuxSpotifyApi.java @@ -16,6 +16,7 @@ * Thanks for LabyStudio for many code snippets. */ public class LinuxSpotifyApi extends AbstractTickSpotifyAPI { + private boolean connected = false; private Track currentTrack; @@ -24,11 +25,11 @@ public class LinuxSpotifyApi extends AbstractTickSpotifyAPI { private long lastTimePositionUpdated; - private MPRISCommunicator MPRISCommunicator = new MPRISCommunicator(); + private final MPRISCommunicator mediaPlayer = new MPRISCommunicator(); @Override - protected void onTick() { - String trackId = MPRISCommunicator.getTrackId(); + protected void onTick() throws Exception { + String trackId = this.mediaPlayer.getTrackId(); // Handle on connect if (!this.connected && !trackId.isEmpty()) { @@ -38,9 +39,9 @@ protected void onTick() { // Handle track changes if (!Objects.equals(trackId, this.currentTrack == null ? null : this.currentTrack.getId())) { - String trackName = MPRISCommunicator.getTrackName(); - String trackArtist = MPRISCommunicator.getArtist(); - int trackLength = MPRISCommunicator.getTrackLength(); + String trackName = this.mediaPlayer.getTrackName(); + String trackArtist = this.mediaPlayer.getArtist(); + int trackLength = this.mediaPlayer.getTrackLength(); boolean isFirstTrack = !this.hasTrack(); @@ -57,7 +58,7 @@ protected void onTick() { } // Handle is playing changes - boolean isPlaying = MPRISCommunicator.isPlaying(); + boolean isPlaying = this.mediaPlayer.isPlaying(); if (isPlaying != this.isPlaying) { this.isPlaying = isPlaying; @@ -66,7 +67,7 @@ protected void onTick() { } - this.updatePosition(MPRISCommunicator.getPosition()); + this.updatePosition(this.mediaPlayer.getPosition()); // Fire keep alive this.listeners.forEach(SpotifyListener::onSync); @@ -96,13 +97,13 @@ public void pressMediaKey(MediaKey mediaKey) { try { switch (mediaKey) { case PLAY_PAUSE: - MPRISCommunicator.playPause(); + this.mediaPlayer.playPause(); break; case NEXT: - MPRISCommunicator.next(); + this.mediaPlayer.next(); break; case PREV: - MPRISCommunicator.previous(); + this.mediaPlayer.previous(); break; } } catch (Exception e) { diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MetadataParser.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusResponse.java similarity index 59% rename from src/main/java/de/labystudio/spotifyapi/platform/linux/api/MetadataParser.java rename to src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusResponse.java index 7ea980a..4beb619 100644 --- a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MetadataParser.java +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusResponse.java @@ -5,76 +5,80 @@ import java.util.List; import java.util.Map; -public class MetadataParser { +public class DBusResponse { private static final String splitRegex = "\\s+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"; - public static Map parse(String input) { - Map result = new HashMap<>(); + private final Map result = new HashMap<>(); - String[] lines = input.split("\n"); + public DBusResponse(String raw) { + String[] lines = raw.split("\n"); int arrayStartPos = 0; String key = ""; Object value = new Object(); - for (int i = 1; i < lines.length-1; i++) { + for (int i = 1; i < lines.length - 1; i++) { String line = lines[i].trim(); - if(arrayStartPos != 0){ - if(line.contains("]")){ + if (arrayStartPos != 0) { + if (line.contains("]")) { StringBuilder arrayString = new StringBuilder(); int j = arrayStartPos; - for (; j < i+1; j++) { + for (; j < i + 1; j++) { arrayString.append(lines[j]).append("\n"); } i = j - 1; arrayStartPos = 0; - value = parseList(arrayString.toString()); - }else continue; - }else if (line.startsWith("variant ")) { + value = this.parseList(arrayString.toString()); + } + } else if (line.startsWith("variant ")) { // Should be value of the dict entry - String[] words = line.replaceFirst("variant {16}", "").split(splitRegex); if (words[0].equals("array")) { arrayStartPos = i; } else { - value = parseValue(words[0], words[1]); + value = this.parseValue(words[0], words[1]); } - }else if (line.startsWith("dict entry(")) { + } else if (line.startsWith("dict entry(")) { // start of dict entry key = ""; value = null; } else if (line.endsWith(")")) { // end of dict entry - result.put(key, value); + this.result.put(key, value); } else { // Should be key of the dict entry - String[] words = line.split(splitRegex); - key = (String) parseValue(words[0], words[1]); + key = (String) this.parseValue(words[0], words[1]); } } + } - return result; + public boolean has(String key) { + return this.result.containsKey(key); } - private static List parseList(String arrayString) { + @SuppressWarnings("unchecked") + public T get(String key) { + return (T) this.result.get(key); + } + + private List parseList(String arrayString) { List result = new ArrayList<>(); String[] lines = arrayString.split("\n"); - - for (int i = 1; i < lines.length-1; i++) { + for (int i = 1; i < lines.length - 1; i++) { String[] words = lines[i].trim().split(splitRegex); - result.add(parseValue(words[0], words[1])); + result.add(this.parseValue(words[0], words[1])); } return result; } - public static Object parseValue(String type, String value) { + private Object parseValue(String type, String value) { switch (type) { case "string": return value.split("\"")[1]; @@ -89,9 +93,8 @@ public static Object parseValue(String type, String value) { } } - public static Object parseValueFromString(String string) { + private Object parseValueFromString(String string) { String[] words = string.split(splitRegex); - - return parseValue(words[0], words[1]); + return this.parseValue(words[0], words[1]); } } diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusSend.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusSend.java new file mode 100644 index 0000000..86ef77b --- /dev/null +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusSend.java @@ -0,0 +1,108 @@ +package de.labystudio.spotifyapi.platform.linux.api; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * Java wrapper for the dbus-send application + *

+ * The dbus-send command is used to send a message to a D-Bus message bus. + * There are two well-known message buses: + * - the systemwide message bus (installed on many systems as the "messagebus" service) + * - the per-user-login-session message bus (started each time a user logs in). + *

+ * The "system" parameter and "session" parameter options direct dbus-send to send messages to the system or session buses respectively. + * If neither is specified, dbus-send sends to the session bus. + *

+ * Nearly all uses of dbus-send must provide the "dest" parameter which is the name of + * a connection on the bus to send the message to. If the "dest" parameter is omitted, no destination is set. + *

+ * The object path and the name of the message to send must always be specified. + * Following arguments, if any, are the message contents (message arguments). + * These are given as type-specified values and may include containers (arrays, dicts, and variants). + * + * @author LabyStudio + */ +public class DBusSend { + + private static final Parameter PARAM_PRINT_REPLY = new Parameter("print-reply"); + private static final InterfaceMember INTERFACE_GET = new InterfaceMember("org.freedesktop.DBus.Properties.Get"); + + private final Parameter[] parameters; + private final String objectPath; + private final Runtime runtime; + + /** + * Creates a new DBusSend API for a specific application + * + * @param parameters The parameters to use + * @param objectPath The object path to use + */ + public DBusSend(Parameter[] parameters, String objectPath) { + this.parameters = parameters; + this.objectPath = objectPath; + this.runtime = Runtime.getRuntime(); + } + + /** + * Request an information from the application + * + * @param keys The requested type of information + * @return The requested information + * @throws Exception If the request failed + */ + public DBusResponse get(String... keys) throws Exception { + String[] contents = new String[keys.length]; + for (int i = 0; i < keys.length; i++) { + contents[i] = "string:'" + keys[i] + "'"; + } + return this.send(INTERFACE_GET, contents); + } + + /** + * Execute an DBusSend command. + * + * @param interfaceMember The interface member to execute + * @param contents The contents to send + * @return The result of the command + * @throws Exception If the command failed + */ + public DBusResponse send(InterfaceMember interfaceMember, String... contents) throws Exception { + // Build arguments + String[] arguments = new String[2 + this.parameters.length + 2 + contents.length]; + arguments[0] = "dbus-send"; + arguments[1] = PARAM_PRINT_REPLY.toString(); + for (int i = 0; i < this.parameters.length; i++) { + arguments[2 + i] = this.parameters[i].toString(); + } + arguments[2 + this.parameters.length] = this.objectPath; + arguments[2 + this.parameters.length + 1] = interfaceMember.toString(); + for (int i = 0; i < contents.length; i++) { + arguments[2 + this.parameters.length + 2 + i] = contents[i]; + } + + // Execute dbus-send process + Process process = this.runtime.exec(arguments); + int exitCode = process.waitFor(); + if (exitCode == 0) { + // Read response + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + return new DBusResponse(builder.toString()); + } else { + // Handle error message + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + String line; + StringBuilder builder = new StringBuilder(); + while ((line = reader.readLine()) != null) { + builder.append(line); + } + throw new Exception("dbus-send execution \"" + String.join(" ", arguments) + "\" failed with exit code " + exitCode + ": " + builder); + } + } + +} diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/InterfaceMember.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/InterfaceMember.java new file mode 100644 index 0000000..206a0e0 --- /dev/null +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/InterfaceMember.java @@ -0,0 +1,20 @@ +package de.labystudio.spotifyapi.platform.linux.api; + +/** + * Interface member wrapper for the DBusSend class. + * + * @author LabyStudio + */ +public class InterfaceMember { + + private final String path; + + public InterfaceMember(String path) { + this.path = path; + } + + public String toString() { + return this.path; + } + +} diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MPRISCommunicator.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MPRISCommunicator.java index cc81970..f5172d0 100644 --- a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MPRISCommunicator.java +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/MPRISCommunicator.java @@ -1,86 +1,66 @@ package de.labystudio.spotifyapi.platform.linux.api; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Map; - -import static de.labystudio.spotifyapi.platform.linux.api.MetadataParser.parse; -import static de.labystudio.spotifyapi.platform.linux.api.MetadataParser.parseValueFromString; +import java.util.List; public class MPRISCommunicator { - public MPRISCommunicator() { - - } - - private final String baseCommand = "dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 "; + private static final Parameter PARAM_DEST = new Parameter("dest", "org.mpris.MediaPlayer2.spotify"); - private Map metadata; + private static final InterfaceMember INTERFACE_PLAY_PAUSE = new InterfaceMember("org.mpris.MediaPlayer2.Player.PlayPause"); + private static final InterfaceMember INTERFACE_NEXT = new InterfaceMember("org.mpris.MediaPlayer2.Player.Next"); + private static final InterfaceMember INTERFACE_PREVIOUS = new InterfaceMember("org.mpris.MediaPlayer2.Player.Previous"); - private String execute(String command) { - StringBuilder output = new StringBuilder(); + private final DBusSend dbus = new DBusSend( + new Parameter[]{ + PARAM_DEST + }, + "/org/mpris/MediaPlayer2" + ); - try { - ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command); - Process process = processBuilder.start(); - process.waitFor(); - - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - - } catch (IOException | InterruptedException e) { - e.printStackTrace(); - } - - return output.substring(output.toString().indexOf('\n') + 1); - } + private DBusResponse metadata; - private void updateMetadata() { - this.metadata = parse(execute(baseCommand + "org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'Metadata'")); + private void updateMetadata() throws Exception { + this.metadata = this.dbus.get("org.mpris.MediaPlayer2.Player", "Metadata"); } - public String getTrackId(){ - updateMetadata(); - return ((String) metadata.get("mpris:trackid")).split("/")[4]; + public String getTrackId() throws Exception { + this.updateMetadata(); + return ((String) this.metadata.get("mpris:trackid")).split("/")[4]; } - public String getTrackName(){ - updateMetadata(); - return metadata.get("xesam:title").toString(); + public String getTrackName() throws Exception { + this.updateMetadata(); + return this.metadata.get("xesam:title").toString(); } - public String getArtist(){ - updateMetadata(); - return String.join(", ", (ArrayList) metadata.get("xesam:artist")); + @SuppressWarnings({"rawtypes", "unchecked"}) + public String getArtist() throws Exception { + this.updateMetadata(); + return String.join(", ", (List) this.metadata.get("xesam:artist")); } - public Integer getTrackLength(){ - updateMetadata(); - return Integer.parseInt(String.valueOf(metadata.get("mpris:length"))) / 1000; + public Integer getTrackLength() throws Exception { + this.updateMetadata(); + return Integer.parseInt(String.valueOf(this.metadata.get("mpris:length"))) / 1000; } - public boolean isPlaying(){ - return parseValueFromString(execute(baseCommand + "org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'PlaybackStatus'").trim().replaceFirst("variant {7}", "")).equals("Playing"); + public boolean isPlaying() throws Exception { + return this.dbus.get("org.mpris.MediaPlayer2.Player", "PlaybackStatus").get("Playing").equals("Playing"); } - public Integer getPosition(){ - return (int) Math.floor(Float.parseFloat((String) parseValueFromString(execute(baseCommand + "org.freedesktop.DBus.Properties.Get string:'org.mpris.MediaPlayer2.Player' string:'Position'").trim().replaceFirst("variant {7}", "")))) / 1000; + public Integer getPosition() throws Exception { + return Integer.parseInt(String.valueOf(this.dbus.get("org.mpris.MediaPlayer2.Player", "Position").get("Position"))) / 1000; } - public void playPause(){ - execute(baseCommand + "org.mpris.MediaPlayer2.Player.PlayPause"); + public void playPause() throws Exception { + this.dbus.send(INTERFACE_PLAY_PAUSE); } - public void next(){ - execute(baseCommand + "org.mpris.MediaPlayer2.Player.Next"); + public void next() throws Exception { + this.dbus.send(INTERFACE_NEXT); } - public void previous(){ - execute(baseCommand + "org.mpris.MediaPlayer2.Player.Previous"); + public void previous() throws Exception { + this.dbus.send(INTERFACE_PREVIOUS); } } diff --git a/src/main/java/de/labystudio/spotifyapi/platform/linux/api/Parameter.java b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/Parameter.java new file mode 100644 index 0000000..2097e0f --- /dev/null +++ b/src/main/java/de/labystudio/spotifyapi/platform/linux/api/Parameter.java @@ -0,0 +1,34 @@ +package de.labystudio.spotifyapi.platform.linux.api; + +/** + * Parameter wrapper for the DBusSend class. + *

+ * It appends the parameter key and value to the command. + * If the value is null, it will only append the key using "--key". + * If the value is not null, it will append the key and value using "--key=value". + * + * @author LabyStudio + */ +public class Parameter { + + private final String key; + private final String value; + + public Parameter(String key, String value) { + this.key = key; + this.value = value; + } + + public Parameter(String key) { + this(key, null); + } + + public String toString() { + return String.format( + "--%s%s", + this.key, + this.value == null ? "" : "=" + this.value + ); + } + +}