Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Linux implementation * Fix wrong TrackID * Fix track position and length * Fix track position * Fix position update not registering * Fix position not syncing with actual Spotify client * Use dbus-send instead of playerctl * Fix README.md, Fix some comment * Follow Java naming conventions, Optimize code * Fix typo in baseCommand * Fix typo in baseCommand, Fix some warnings * Fix spacing in code * Fix spacing in code (again) * Wrap D-Bus communication into class (to MPRISCommunicator) * improve dbus & mpris api (wip) * implement variant parser, fix playback parsing on linux * Fix track position not updating frequently * Fix position, Fix position updating while not playing * Revert "Fix position updating while not playing" * Fix handling position changes not working as intended * Fix disconnecting when Play/Pausing * Rename SpotifyActionTest.java to SpotifyPlayPauseTest.java * Rename SpotifyActionTest class to SpotifyPlayPauseTest * use this.getPosition() because we want to fire an positionChanged event if the media player interrupts or changes its current "direction". added a getPosition output line at onSync in SpotifyListenerTest to debug its current calculated position * version 1.2.0 --------- Co-authored-by: LabyStudio <labystudio@gmail.com>
- Loading branch information
1 parent
c93d6ea
commit 941cfa2
Showing
12 changed files
with
788 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
src/main/java/de/labystudio/spotifyapi/platform/linux/LinuxSpotifyApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
package de.labystudio.spotifyapi.platform.linux; | ||
|
||
import de.labystudio.spotifyapi.SpotifyListener; | ||
import de.labystudio.spotifyapi.model.MediaKey; | ||
import de.labystudio.spotifyapi.model.Track; | ||
import de.labystudio.spotifyapi.platform.AbstractTickSpotifyAPI; | ||
import de.labystudio.spotifyapi.platform.linux.api.MPRISCommunicator; | ||
|
||
import java.util.Objects; | ||
|
||
/** | ||
* Linux implementation of the SpotifyAPI. | ||
* It uses the MPRIS to access the Spotify's media control and metadata. | ||
* | ||
* @author holybaechu, LabyStudio | ||
* Thanks for LabyStudio for many code snippets. | ||
*/ | ||
public class LinuxSpotifyApi extends AbstractTickSpotifyAPI { | ||
|
||
private boolean connected = false; | ||
|
||
private Track currentTrack; | ||
private int currentPosition = -1; | ||
private boolean isPlaying; | ||
|
||
private long lastTimePositionUpdated; | ||
|
||
private final MPRISCommunicator mediaPlayer = new MPRISCommunicator(); | ||
|
||
@Override | ||
protected void onTick() throws Exception { | ||
String trackId = this.mediaPlayer.getTrackId(); | ||
|
||
// Handle on connect | ||
if (!this.connected && !trackId.isEmpty()) { | ||
this.connected = true; | ||
this.listeners.forEach(SpotifyListener::onConnect); | ||
} | ||
|
||
// Handle track changes | ||
if (!Objects.equals(trackId, this.currentTrack == null ? null : this.currentTrack.getId())) { | ||
String trackName = this.mediaPlayer.getTrackName(); | ||
String trackArtist = this.mediaPlayer.getArtist(); | ||
int trackLength = this.mediaPlayer.getTrackLength(); | ||
|
||
boolean isFirstTrack = !this.hasTrack(); | ||
|
||
Track track = new Track(trackId, trackName, trackArtist, trackLength); | ||
this.currentTrack = track; | ||
|
||
// Fire on track changed | ||
this.listeners.forEach(listener -> listener.onTrackChanged(track)); | ||
|
||
// Reset position on song change | ||
if (!isFirstTrack) { | ||
this.updatePosition(0); | ||
} | ||
} | ||
|
||
// Handle is playing changes | ||
boolean isPlaying = this.mediaPlayer.isPlaying(); | ||
if (isPlaying != this.isPlaying) { | ||
this.isPlaying = isPlaying; | ||
|
||
// Fire on play back changed | ||
this.listeners.forEach(listener -> listener.onPlayBackChanged(isPlaying)); | ||
} | ||
|
||
// Handle position changes | ||
int position = this.mediaPlayer.getPosition(); | ||
if (!this.hasPosition() || Math.abs(position - this.getPosition()) >= 1000) { | ||
this.updatePosition(position); | ||
} | ||
|
||
// Fire keep alive | ||
this.listeners.forEach(SpotifyListener::onSync); | ||
} | ||
|
||
@Override | ||
public void stop() { | ||
super.stop(); | ||
this.connected = false; | ||
} | ||
|
||
private void updatePosition(int position) { | ||
if (position == this.currentPosition) { | ||
return; | ||
} | ||
|
||
// Update position known state | ||
this.currentPosition = position; | ||
this.lastTimePositionUpdated = System.currentTimeMillis(); | ||
|
||
// Fire on position changed | ||
this.listeners.forEach(listener -> listener.onPositionChanged(position)); | ||
} | ||
|
||
@Override | ||
public void pressMediaKey(MediaKey mediaKey) { | ||
try { | ||
switch (mediaKey) { | ||
case PLAY_PAUSE: | ||
this.mediaPlayer.playPause(); | ||
break; | ||
case NEXT: | ||
this.mediaPlayer.next(); | ||
break; | ||
case PREV: | ||
this.mediaPlayer.previous(); | ||
break; | ||
} | ||
} catch (Exception e) { | ||
this.listeners.forEach(listener -> listener.onDisconnect(e)); | ||
this.connected = false; | ||
} | ||
} | ||
|
||
@Override | ||
public int getPosition() { | ||
if (!this.hasPosition()) { | ||
throw new IllegalStateException("Position is not known yet"); | ||
} | ||
|
||
if (this.isPlaying) { | ||
// Interpolate position | ||
long timePassed = System.currentTimeMillis() - this.lastTimePositionUpdated; | ||
return this.currentPosition + (int) timePassed; | ||
} else { | ||
return this.currentPosition; | ||
} | ||
} | ||
|
||
@Override | ||
public Track getTrack() { | ||
return this.currentTrack; | ||
} | ||
|
||
@Override | ||
public boolean isPlaying() { | ||
return this.isPlaying; | ||
} | ||
|
||
@Override | ||
public boolean isConnected() { | ||
return this.connected; | ||
} | ||
|
||
@Override | ||
public boolean hasPosition() { | ||
return this.currentPosition != -1; | ||
} | ||
|
||
} |
114 changes: 114 additions & 0 deletions
114
src/main/java/de/labystudio/spotifyapi/platform/linux/api/DBusSend.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package de.labystudio.spotifyapi.platform.linux.api; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.InputStreamReader; | ||
|
||
/** | ||
* Java wrapper for the dbus-send application | ||
* <p> | ||
* 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). | ||
* <p> | ||
* 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. | ||
* <p> | ||
* 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. | ||
* <p> | ||
* 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 Variant get(String... keys) throws Exception { | ||
String[] contents = new String[keys.length]; | ||
for (int i = 0; i < keys.length; i++) { | ||
contents[i] = String.format("string:%s", 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 Variant 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 response; | ||
while ((response = reader.readLine()) != null) { | ||
if (response.startsWith("method ")) { | ||
continue; | ||
} | ||
builder.append(response).append("\n"); | ||
} | ||
if (builder.toString().isEmpty()) { | ||
return new Variant("success", true); | ||
} | ||
return Variant.parse(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); | ||
} | ||
} | ||
|
||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/de/labystudio/spotifyapi/platform/linux/api/InterfaceMember.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
||
} |
Oops, something went wrong.