Skip to content

Commit b933939

Browse files
committed
Added API endpoints to indicate the current session state + API server is available immediately upon startup (#166)
1 parent 5cfae00 commit b933939

10 files changed

Lines changed: 212 additions & 50 deletions

File tree

api/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ This module depends on `librespot-core` and provides an API to interact with the
55

66
## Available endpoints
77

8+
All the endpoints will respond with `200` if successful or `204` if there isn't any active session.
9+
810
### Player
911
- `POST \player\load` Load a track from a given uri. The request body should contain two parameters: `uri` and `play`.
1012
- `POST \player\pause` Pause playback.
@@ -30,6 +32,9 @@ The currently available events are:
3032
- `trackSeeked`
3133
- `metadataAvailable`
3234
- `playbackHaltStateChanged`
35+
- `sessionCleared`
36+
- `sessionChanged`
37+
- `inactiveSession`
3338

3439
## Examples
3540

api/src/main/java/xyz/gianlu/librespot/api/ApiServer.java

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,29 @@
99
import xyz.gianlu.librespot.api.handlers.EventsHandler;
1010
import xyz.gianlu.librespot.api.handlers.MetadataHandler;
1111
import xyz.gianlu.librespot.api.handlers.PlayerHandler;
12-
import xyz.gianlu.librespot.core.Session;
1312

1413
public class ApiServer {
1514
private static final Logger LOGGER = Logger.getLogger(ApiServer.class);
1615
private final int port;
1716
private final String host;
17+
private final RoutingHandler handler;
1818
private Undertow undertow = null;
1919

20-
public ApiServer(@NotNull ApiConfiguration conf) {
20+
public ApiServer(@NotNull ApiConfiguration conf, @NotNull SessionWrapper wrapper) {
2121
this.port = conf.apiPort();
2222
this.host = conf.apiHost();
2323

24-
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
25-
if (undertow != null) undertow.stop();
26-
}));
27-
}
24+
EventsHandler events = new EventsHandler();
25+
wrapper.setListener(events);
2826

29-
private static void prepareHandlers(@NotNull RoutingHandler root, @NotNull Session session) {
30-
root.post("/player/{cmd}", new PlayerHandler(session))
31-
.post("/metadata/{type}/{uri}", new MetadataHandler(session))
32-
.get("/events", new EventsHandler(session));
27+
handler = new RoutingHandler();
28+
handler.post("/player/{cmd}", new PlayerHandler(wrapper))
29+
.post("/metadata/{type}/{uri}", new MetadataHandler(wrapper))
30+
.get("/events", events);
3331
}
3432

35-
public void start(@NotNull Session session) {
36-
RoutingHandler handler = new RoutingHandler();
37-
prepareHandlers(handler, session);
33+
public void start() {
34+
if (undertow != null) throw new IllegalStateException("Already started!");
3835

3936
Filter corsFilter = new Filter(handler);
4037
corsFilter.setPolicyClass(AllowAll.class.getCanonicalName());
@@ -54,9 +51,4 @@ public void stop() {
5451

5552
LOGGER.info("Server stopped!");
5653
}
57-
58-
public void restart(@NotNull Session session) {
59-
stop();
60-
start(session);
61-
}
6254
}

api/src/main/java/xyz/gianlu/librespot/api/Main.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ public class Main {
1717

1818
public static void main(String[] args) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException {
1919
AbsConfiguration conf = new FileConfiguration(args);
20-
ApiServer server = new ApiServer(conf);
21-
if (conf.authStrategy() == AuthConfiguration.Strategy.ZEROCONF) {
22-
ZeroconfServer.create(conf).addSessionListener(server::restart);
23-
} else {
24-
server.start(new Session.Builder(conf).create());
25-
}
20+
21+
SessionWrapper wrapper;
22+
if (conf.authStrategy() == AuthConfiguration.Strategy.ZEROCONF)
23+
wrapper = SessionWrapper.fromZeroconf(ZeroconfServer.create(conf));
24+
else
25+
wrapper = SessionWrapper.fromSession(new Session.Builder(conf).create());
26+
27+
ApiServer server = new ApiServer(conf, wrapper);
28+
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
29+
server.start();
2630
}
2731
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package xyz.gianlu.librespot.api;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
5+
import xyz.gianlu.librespot.core.Session;
6+
import xyz.gianlu.librespot.core.ZeroconfServer;
7+
8+
import java.util.concurrent.atomic.AtomicReference;
9+
10+
/**
11+
* @author Gianlu
12+
*/
13+
public final class SessionWrapper {
14+
private final AtomicReference<Session> ref = new AtomicReference<>(null);
15+
private Listener listener = null;
16+
17+
private SessionWrapper() {
18+
}
19+
20+
/**
21+
* Convenience method to create an instance of {@link SessionWrapper} that is updated by {@link ZeroconfServer}
22+
*
23+
* @param server The {@link ZeroconfServer}
24+
* @return A wrapper that holds a changing session
25+
*/
26+
@NotNull
27+
public static SessionWrapper fromZeroconf(@NotNull ZeroconfServer server) {
28+
SessionWrapper wrapper = new SessionWrapper();
29+
server.addSessionListener(wrapper::set);
30+
return wrapper;
31+
}
32+
33+
/**
34+
* Convenience method to create an instance of {@link SessionWrapper} that holds a static session
35+
*
36+
* @param session The static session
37+
* @return A wrapper that holds a never-changing session
38+
*/
39+
@NotNull
40+
public static SessionWrapper fromSession(@NotNull Session session) {
41+
SessionWrapper wrapper = new SessionWrapper();
42+
wrapper.ref.set(session);
43+
return wrapper;
44+
}
45+
46+
public void setListener(@NotNull Listener listener) {
47+
this.listener = listener;
48+
49+
Session s;
50+
if ((s = ref.get()) != null) listener.onNewSession(s);
51+
}
52+
53+
private void set(@NotNull Session session) {
54+
ref.set(session);
55+
session.addCloseListener(this::clear);
56+
if (listener != null) listener.onNewSession(session);
57+
}
58+
59+
private void clear() {
60+
ref.set(null);
61+
if (listener != null) listener.onSessionCleared();
62+
}
63+
64+
@Nullable
65+
public Session get() {
66+
Session s = ref.get();
67+
if (s != null) {
68+
if (s.valid()) return s;
69+
else clear();
70+
}
71+
72+
return null;
73+
}
74+
75+
public interface Listener {
76+
void onSessionCleared();
77+
78+
void onNewSession(@NotNull Session session);
79+
}
80+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package xyz.gianlu.librespot.api.handlers;
2+
3+
import io.undertow.server.HttpHandler;
4+
import io.undertow.server.HttpServerExchange;
5+
import io.undertow.util.StatusCodes;
6+
import org.jetbrains.annotations.NotNull;
7+
import xyz.gianlu.librespot.api.SessionWrapper;
8+
import xyz.gianlu.librespot.core.Session;
9+
10+
/**
11+
* @author Gianlu
12+
*/
13+
public abstract class AbsSessionHandler implements HttpHandler {
14+
private final SessionWrapper wrapper;
15+
16+
public AbsSessionHandler(@NotNull SessionWrapper wrapper) {
17+
this.wrapper = wrapper;
18+
}
19+
20+
@Override
21+
public final void handleRequest(HttpServerExchange exchange) throws Exception {
22+
Session s = wrapper.get();
23+
if (s == null) {
24+
exchange.setStatusCode(StatusCodes.NO_CONTENT);
25+
return;
26+
}
27+
28+
handleRequest(exchange, s);
29+
}
30+
31+
protected abstract void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception;
32+
}

api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
import org.apache.log4j.Logger;
1010
import org.jetbrains.annotations.NotNull;
1111
import org.jetbrains.annotations.Nullable;
12+
import xyz.gianlu.librespot.api.SessionWrapper;
1213
import xyz.gianlu.librespot.common.ProtobufToJson;
1314
import xyz.gianlu.librespot.core.Session;
1415
import xyz.gianlu.librespot.mercury.model.PlayableId;
1516
import xyz.gianlu.librespot.player.Player;
1617

17-
public final class EventsHandler extends WebSocketProtocolHandshakeHandler implements Player.EventsListener {
18+
public final class EventsHandler extends WebSocketProtocolHandshakeHandler implements Player.EventsListener, SessionWrapper.Listener {
1819
private static final Logger LOGGER = Logger.getLogger(EventsHandler.class);
1920

20-
public EventsHandler(@NotNull Session session) {
21+
public EventsHandler() {
2122
super((WebSocketConnectionCallback) (exchange, channel) -> LOGGER.info(String.format("Accepted new websocket connection from %s.", channel.getSourceAddress().getAddress())));
22-
session.player().addEventsListener(this);
2323
}
2424

2525
private void dispatch(@NotNull JsonObject obj) {
@@ -86,4 +86,29 @@ public void onPlaybackHaltStateChanged(boolean halted, long trackTime) {
8686
obj.addProperty("halted", halted);
8787
dispatch(obj);
8888
}
89+
90+
@Override
91+
public void onInactiveSession(boolean timeout) {
92+
JsonObject obj = new JsonObject();
93+
obj.addProperty("event", "inactiveSession");
94+
obj.addProperty("timeout", timeout);
95+
dispatch(obj);
96+
}
97+
98+
@Override
99+
public void onSessionCleared() {
100+
JsonObject obj = new JsonObject();
101+
obj.addProperty("event", "sessionCleared");
102+
dispatch(obj);
103+
}
104+
105+
@Override
106+
public void onNewSession(@NotNull Session session) {
107+
JsonObject obj = new JsonObject();
108+
obj.addProperty("event", "sessionChanged");
109+
obj.addProperty("username", session.username());
110+
dispatch(obj);
111+
112+
session.player().addEventsListener(this);
113+
}
89114
}

api/src/main/java/xyz/gianlu/librespot/api/handlers/MetadataHandler.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package xyz.gianlu.librespot.api.handlers;
22

33
import com.google.gson.JsonObject;
4-
import io.undertow.server.HttpHandler;
54
import io.undertow.server.HttpServerExchange;
65
import org.apache.log4j.Logger;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
8+
import xyz.gianlu.librespot.api.SessionWrapper;
99
import xyz.gianlu.librespot.api.Utils;
1010
import xyz.gianlu.librespot.common.ProtobufToJson;
1111
import xyz.gianlu.librespot.core.Session;
@@ -21,16 +21,15 @@
2121
/**
2222
* @author Gianlu
2323
*/
24-
public final class MetadataHandler implements HttpHandler {
24+
public final class MetadataHandler extends AbsSessionHandler {
2525
private static final Logger LOGGER = Logger.getLogger(MetadataHandler.class);
26-
private final Session session;
2726

28-
public MetadataHandler(@NotNull Session session) {
29-
this.session = session;
27+
public MetadataHandler(@NotNull SessionWrapper wrapper) {
28+
super(wrapper);
3029
}
3130

3231
@Override
33-
public void handleRequest(HttpServerExchange exchange) throws Exception {
32+
public void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception {
3433
exchange.startBlocking();
3534
if (exchange.isInIoThread()) {
3635
exchange.dispatch(this);
@@ -57,7 +56,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
5756
}
5857

5958
try {
60-
JsonObject obj = handle(type, uri);
59+
JsonObject obj = handle(session, type, uri);
6160
exchange.getResponseSender().send(obj.toString());
6261
} catch (ApiClient.StatusCodeException ex) {
6362
if (ex.code == 404) {
@@ -76,7 +75,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
7675
}
7776

7877
@NotNull
79-
private JsonObject handle(@NotNull MetadataType type, @NotNull String uri) throws IOException, MercuryClient.MercuryException, IllegalArgumentException {
78+
private JsonObject handle(@NotNull Session session, @NotNull MetadataType type, @NotNull String uri) throws IOException, MercuryClient.MercuryException, IllegalArgumentException {
8079
switch (type) {
8180
case ALBUM:
8281
return ProtobufToJson.convert(session.api().getMetadata4Album(AlbumId.fromUri(uri)));

api/src/main/java/xyz/gianlu/librespot/api/handlers/PlayerHandler.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import com.google.gson.JsonObject;
44
import com.spotify.metadata.proto.Metadata;
5-
import io.undertow.server.HttpHandler;
65
import io.undertow.server.HttpServerExchange;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
8+
import xyz.gianlu.librespot.api.SessionWrapper;
99
import xyz.gianlu.librespot.api.Utils;
1010
import xyz.gianlu.librespot.common.ProtobufToJson;
1111
import xyz.gianlu.librespot.core.Session;
@@ -18,14 +18,13 @@
1818
import java.util.Map;
1919
import java.util.Objects;
2020

21-
public final class PlayerHandler implements HttpHandler {
22-
private final Session session;
21+
public final class PlayerHandler extends AbsSessionHandler {
2322

24-
public PlayerHandler(@NotNull Session session) {
25-
this.session = session;
23+
public PlayerHandler(@NotNull SessionWrapper wrapper) {
24+
super(wrapper);
2625
}
2726

28-
private void setVolume(HttpServerExchange exchange, @Nullable String valStr) {
27+
private void setVolume(HttpServerExchange exchange, Session session, @Nullable String valStr) {
2928
if (valStr == null) {
3029
Utils.invalidParameter(exchange, "volume");
3130
return;
@@ -47,7 +46,7 @@ private void setVolume(HttpServerExchange exchange, @Nullable String valStr) {
4746
session.player().setVolume(val);
4847
}
4948

50-
private void load(HttpServerExchange exchange, @Nullable String uri, boolean play) {
49+
private void load(HttpServerExchange exchange, Session session, @Nullable String uri, boolean play) {
5150
if (uri == null) {
5251
Utils.invalidParameter(exchange, "uri");
5352
return;
@@ -56,7 +55,7 @@ private void load(HttpServerExchange exchange, @Nullable String uri, boolean pla
5655
session.player().load(uri, play);
5756
}
5857

59-
private void current(HttpServerExchange exchange) {
58+
private void current(HttpServerExchange exchange, Session session) {
6059
PlayableId id = session.player().currentPlayableId();
6160

6261
JsonObject obj = new JsonObject();
@@ -90,7 +89,7 @@ private void current(HttpServerExchange exchange) {
9089
}
9190

9291
@Override
93-
public void handleRequest(HttpServerExchange exchange) throws Exception {
92+
protected void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception {
9493
exchange.startBlocking();
9594
if (exchange.isInIoThread()) {
9695
exchange.dispatch(this);
@@ -112,10 +111,10 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
112111

113112
switch (cmd) {
114113
case CURRENT:
115-
current(exchange);
114+
current(exchange, session);
116115
return;
117116
case SET_VOLUME:
118-
setVolume(exchange, Utils.getFirstString(params, "volume"));
117+
setVolume(exchange, session, Utils.getFirstString(params, "volume"));
119118
return;
120119
case VOLUME_UP:
121120
session.player().volumeUp();
@@ -124,7 +123,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
124123
session.player().volumeDown();
125124
return;
126125
case LOAD:
127-
load(exchange, Utils.getFirstString(params, "uri"), Utils.getFirstBoolean(params, "play"));
126+
load(exchange, session, Utils.getFirstString(params, "uri"), Utils.getFirstBoolean(params, "play"));
128127
return;
129128
case PAUSE:
130129
session.player().pause();

0 commit comments

Comments
 (0)