Skip to content

Commit ae2ceb9

Browse files
committed
implement macOS support, version 1.1.0
1 parent 6f4c166 commit ae2ceb9

File tree

10 files changed

+458
-101
lines changed

10 files changed

+458
-101
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ because the API reads the information directly from the application itself.
1414

1515
#### Supported operating systems:
1616
- Windows
17+
- macOS
1718

1819
## Gradle Setup
1920
```groovy
@@ -22,7 +23,7 @@ repositories {
2223
}
2324
2425
dependencies {
25-
implementation 'com.github.LabyStudio:java-spotify-api:1.0.9:all'
26+
implementation 'com.github.LabyStudio:java-spotify-api:1.1.0:all'
2627
}
2728
```
2829

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group 'de.labystudio'
7-
version '1.0.9'
7+
version '1.1.0'
88

99
compileJava {
1010
sourceCompatibility = '1.8'

src/main/java/de/labystudio/spotifyapi/platform/AbstractSpotifyAPI.java

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package de.labystudio.spotifyapi.platform;
2+
3+
import de.labystudio.spotifyapi.SpotifyAPI;
4+
import de.labystudio.spotifyapi.SpotifyListener;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.concurrent.Executors;
9+
import java.util.concurrent.ScheduledFuture;
10+
11+
/**
12+
* Abstract tick class for SpotifyAPI implementations.
13+
*
14+
* @author LabyStudio
15+
*/
16+
public abstract class AbstractTickSpotifyAPI implements SpotifyAPI {
17+
18+
/**
19+
* The list of all Spotify listeners.
20+
*/
21+
protected final List<SpotifyListener> listeners = new ArrayList<>();
22+
23+
private ScheduledFuture<?> task;
24+
25+
/**
26+
* Initialize the SpotifyAPI abstract tick implementation.
27+
* It will create a task that will update the current track and position every second.
28+
*
29+
* @return the initialized SpotifyAPI
30+
* @throws IllegalStateException if the API is already initialized
31+
*/
32+
public SpotifyAPI initialize() {
33+
if (this.isInitialized()) {
34+
throw new IllegalStateException("The SpotifyAPI is already initialized");
35+
}
36+
37+
// Initial tick
38+
this.onTick();
39+
40+
// Start task to update every second
41+
this.task = Executors.newScheduledThreadPool(1).scheduleWithFixedDelay(
42+
this::onTick, 1, 1, java.util.concurrent.TimeUnit.SECONDS
43+
);
44+
45+
return this;
46+
}
47+
48+
protected abstract void onTick();
49+
50+
@Override
51+
public void registerListener(SpotifyListener listener) {
52+
this.listeners.add(listener);
53+
}
54+
55+
@Override
56+
public void unregisterListener(SpotifyListener listener) {
57+
this.listeners.remove(listener);
58+
}
59+
60+
@Override
61+
public boolean isInitialized() {
62+
return this.task != null;
63+
}
64+
65+
@Override
66+
public void stop() {
67+
if (this.task != null) {
68+
this.task.cancel(true);
69+
this.task = null;
70+
}
71+
}
72+
}
Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,151 @@
11
package de.labystudio.spotifyapi.platform.osx;
22

3-
import de.labystudio.spotifyapi.SpotifyAPI;
3+
import de.labystudio.spotifyapi.SpotifyListener;
44
import de.labystudio.spotifyapi.model.MediaKey;
55
import de.labystudio.spotifyapi.model.Track;
6-
import de.labystudio.spotifyapi.platform.AbstractSpotifyAPI;
6+
import de.labystudio.spotifyapi.platform.AbstractTickSpotifyAPI;
7+
import de.labystudio.spotifyapi.platform.osx.api.spotify.SpotifyAppleScript;
8+
9+
import java.util.Objects;
710

811
/**
912
* OSX implementation of the SpotifyAPI.
13+
* It uses the AppleScript API to access the Spotify application.
1014
*
1115
* @author LabyStudio
1216
*/
13-
public class OSXSpotifyApi extends AbstractSpotifyAPI {
17+
public class OSXSpotifyApi extends AbstractTickSpotifyAPI {
1418

15-
public OSXSpotifyApi() {
16-
super();
17-
}
19+
private final SpotifyAppleScript appleScript = new SpotifyAppleScript();
20+
21+
private boolean connected = false;
22+
23+
private Track currentTrack;
24+
private int currentPosition = -1;
25+
private boolean isPlaying;
26+
27+
private long lastTimePositionUpdated;
1828

1929
@Override
20-
public SpotifyAPI initialize() {
21-
return this;
30+
protected void onTick() {
31+
try {
32+
String trackId = this.appleScript.getTrackId();
33+
34+
// Handle on connect
35+
if (!this.connected && !trackId.isEmpty()) {
36+
this.connected = true;
37+
this.listeners.forEach(SpotifyListener::onConnect);
38+
}
39+
40+
// Handle track changes
41+
if (!Objects.equals(trackId, this.currentTrack == null ? null : this.currentTrack.getId())) {
42+
String trackName = this.appleScript.getTrackName();
43+
String trackArtist = this.appleScript.getTrackArtist();
44+
int trackLength = this.appleScript.getTrackLength();
45+
46+
boolean isFirstTrack = !this.hasTrack();
47+
48+
Track track = new Track(trackId, trackName, trackArtist, trackLength);
49+
this.currentTrack = track;
50+
51+
// Fire on track changed
52+
this.listeners.forEach(listener -> listener.onTrackChanged(track));
53+
54+
// Reset position on song change
55+
if (!isFirstTrack) {
56+
this.updatePosition(0);
57+
}
58+
}
59+
60+
// Handle is playing changes
61+
boolean isPlaying = this.appleScript.getPlayerState();
62+
if (isPlaying != this.isPlaying) {
63+
this.isPlaying = isPlaying;
64+
65+
// Fire on play back changed
66+
this.listeners.forEach(listener -> listener.onPlayBackChanged(isPlaying));
67+
}
68+
69+
// Handle position changes
70+
int position = this.appleScript.getPlayerPosition();
71+
if (!this.hasPosition() || Math.abs(position - this.getPosition()) > 1000) {
72+
this.updatePosition(position);
73+
}
74+
75+
// Fire keep alive
76+
this.listeners.forEach(SpotifyListener::onSync);
77+
} catch (Exception e) {
78+
this.listeners.forEach(listener -> listener.onDisconnect(e));
79+
this.connected = false;
80+
}
2281
}
2382

24-
@Override
25-
public boolean isInitialized() {
26-
return false;
83+
private void updatePosition(int position) {
84+
if (position == this.currentPosition) {
85+
return;
86+
}
87+
88+
// Update position known state
89+
this.currentPosition = position;
90+
this.lastTimePositionUpdated = System.currentTimeMillis();
91+
92+
// Fire on position changed
93+
this.listeners.forEach(listener -> listener.onPositionChanged(position));
2794
}
2895

2996
@Override
30-
public Track getTrack() {
31-
return null; // TODO Implement OSX SpotifyAPI
97+
public void pressMediaKey(MediaKey mediaKey) {
98+
try {
99+
switch (mediaKey) {
100+
case PLAY_PAUSE:
101+
this.appleScript.playPause();
102+
break;
103+
case NEXT:
104+
this.appleScript.nextTrack();
105+
break;
106+
case PREV:
107+
this.appleScript.previousTrack();
108+
break;
109+
}
110+
} catch (Exception e) {
111+
this.listeners.forEach(listener -> listener.onDisconnect(e));
112+
this.connected = false;
113+
}
32114
}
33115

34116
@Override
35117
public int getPosition() {
36-
return 0; // TODO Implement OSX SpotifyAPI
37-
}
118+
if (!this.hasPosition()) {
119+
throw new IllegalStateException("Position is not known yet");
120+
}
38121

39-
@Override
40-
public boolean hasPosition() {
41-
return false; // TODO Implement OSX SpotifyAPI
122+
if (this.isPlaying) {
123+
// Interpolate position
124+
long timePassed = System.currentTimeMillis() - this.lastTimePositionUpdated;
125+
return this.currentPosition + (int) timePassed;
126+
} else {
127+
return this.currentPosition;
128+
}
42129
}
43130

44131
@Override
45-
public void pressMediaKey(MediaKey mediaKey) {
46-
132+
public Track getTrack() {
133+
return this.currentTrack;
47134
}
48135

49136
@Override
50137
public boolean isPlaying() {
51-
return false; // TODO Implement OSX SpotifyAPI
138+
return this.isPlaying;
52139
}
53140

54141
@Override
55142
public boolean isConnected() {
56-
return false; // TODO Implement OSX SpotifyAPI
143+
return this.connected;
57144
}
58145

59146
@Override
60-
public void stop() {
61-
147+
public boolean hasPosition() {
148+
return this.currentPosition != -1;
62149
}
63150

64151
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package de.labystudio.spotifyapi.platform.osx.api;
2+
3+
4+
/**
5+
* Wrapper for the AppleScript parameters.
6+
*
7+
* @author LabyStudio
8+
*/
9+
public class Action {
10+
11+
public static final Action GET = new Action("get", "the");
12+
public static final Action OF = new Action("of");
13+
14+
private final String action;
15+
16+
public Action(String... args) {
17+
this.action = String.join(" ", args);
18+
}
19+
20+
public Action(String argument) {
21+
this.action = argument;
22+
}
23+
24+
public Action(Action... actions) {
25+
this.action = toString(actions);
26+
}
27+
28+
@Override
29+
public String toString() {
30+
return this.action;
31+
}
32+
33+
/**
34+
* Convert an array of actions to a string.
35+
*
36+
* @param actions The actions to convert
37+
* @return The string representation of the actions
38+
*/
39+
public static String toString(Action... actions) {
40+
String[] args = new String[actions.length];
41+
for (int i = 0; i < actions.length; i++) {
42+
args[i] = actions[i].toString();
43+
}
44+
return String.join(" ", args);
45+
}
46+
}

0 commit comments

Comments
 (0)