Skip to content

Commit

Permalink
Added connectionDropped and connectionEstablished events + better…
Browse files Browse the repository at this point in the history
… response codes for API + updated api/README (#172)
  • Loading branch information
devgianlu committed Jan 26, 2020
1 parent 71a8dbb commit 9c9842a
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 24 deletions.
32 changes: 17 additions & 15 deletions api/README.md
Expand Up @@ -4,11 +4,13 @@
This module depends on `librespot-core` and provides an API to interact with the Spotify client.

## Available endpoints

All the endpoints will respond with `200` if successful or `204` if there isn't any active session.
All the endpoints will respond with `200` if successful or:
- `204`, if there isn't any active session (Zeroconf only)
- `500`, if the session is invalid
- `503`, if the session is reconnecting (`Retry-After` is always 10 seconds)

### Player
- `POST \player\load` Load a track from a given uri. The request body should contain two parameters: `uri` and `play`.
- `POST \player\load` Load a track from a given URI. The request body should contain two parameters: `uri` and `play`.
- `POST \player\pause` Pause playback.
- `POST \player\resume` Resume playback.
- `POST \player\next` Skip to next track.
Expand All @@ -28,22 +30,22 @@ All the endpoints will respond with `200` if successful or `204` if there isn't
- `POST \token\{scope}` Request an access token for a specific scope.

### Events

You can subscribe for players events by creating a WebSocket connection to `/events`.
The currently available events are:
- `contextChanged`
- `trackChanged`
- `playbackPaused`
- `playbackResumed`
- `trackSeeked`
- `metadataAvailable`
- `playbackHaltStateChanged`
- `sessionCleared`
- `sessionChanged`
- `inactiveSession`
- `contextChanged`, the Spotify context URI changed
- `trackChanged`, the Spotify track URI changed
- `playbackPaused`, playback has been paused
- `playbackResumed`, playback has been resumed
- `trackSeeked`, track has been seeked
- `metadataAvailable`, metadata for the current track is available
- `playbackHaltStateChanged`, playback halted or resumed from halt
- `sessionCleared`, (Zeroconf only) current session went away
- `sessionChanged`, (Zeroconf only) current session changed
- `inactiveSession`, current session is now inactive (no audio)
- `connectionDropped`, a network error occurred and we're trying to reconnect
- `connectionEstablished`, successfully reconnected

## Examples

`curl -X POST -d "uri=spotify:track:xxxxxxxxxxxxxxxxxxxxxx&play=true" http://localhost:24879/player/load`

`curl -X POST http://localhost:24879/metadata/track/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`
Expand Up @@ -2,6 +2,7 @@

import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.StatusCodes;
import org.jetbrains.annotations.NotNull;
import xyz.gianlu.librespot.api.SessionWrapper;
Expand All @@ -25,6 +26,17 @@ public final void handleRequest(HttpServerExchange exchange) throws Exception {
return;
}

if (s.reconnecting()) {
exchange.setStatusCode(StatusCodes.SERVICE_UNAVAILABLE);
exchange.getResponseHeaders().add(Headers.RETRY_AFTER, 10);
return;
}

if (!s.valid()) {
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
return;
}

handleRequest(exchange, s);
}

Expand Down
Expand Up @@ -15,7 +15,7 @@
import xyz.gianlu.librespot.mercury.model.PlayableId;
import xyz.gianlu.librespot.player.Player;

public final class EventsHandler extends WebSocketProtocolHandshakeHandler implements Player.EventsListener, SessionWrapper.Listener {
public final class EventsHandler extends WebSocketProtocolHandshakeHandler implements Player.EventsListener, SessionWrapper.Listener, Session.ReconnectionListener {
private static final Logger LOGGER = Logger.getLogger(EventsHandler.class);

public EventsHandler() {
Expand Down Expand Up @@ -110,5 +110,20 @@ public void onNewSession(@NotNull Session session) {
dispatch(obj);

session.player().addEventsListener(this);
session.addReconnectionListener(this);
}

@Override
public void onConnectionDropped() {
JsonObject obj = new JsonObject();
obj.addProperty("event", "connectionDropped");
dispatch(obj);
}

@Override
public void onConnectionEstablished() {
JsonObject obj = new JsonObject();
obj.addProperty("event", "connectionEstablished");
dispatch(obj);
}
}
Expand Up @@ -45,12 +45,6 @@ protected void appendToQueue(@NotNull Packet packet) {

protected abstract void exception(@NotNull Exception ex);

private static final class LooperException extends Exception {
private LooperException(Throwable cause) {
super(cause);
}
}

private final class Looper implements Runnable {
private volatile boolean shouldStop = false;

Expand All @@ -66,8 +60,7 @@ public void run() {
exception(ex);
}
});
} catch (InterruptedException ex) {
executorService.execute(() -> exception(new LooperException(ex)));
} catch (InterruptedException ignored) {
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions core/src/main/java/xyz/gianlu/librespot/core/Session.java
Expand Up @@ -76,6 +76,7 @@ public final class Session implements Closeable {
private final AtomicBoolean authLock = new AtomicBoolean(false);
private final OkHttpClient client;
private final List<CloseListener> closeListeners = Collections.synchronizedList(new ArrayList<>());
private final List<ReconnectionListener> reconnectionListeners = Collections.synchronizedList(new ArrayList<>());
private ConnectionHolder conn;
private CipherPair cipherPair;
private Receiver receiver;
Expand Down Expand Up @@ -528,6 +529,10 @@ public boolean valid() {
return apWelcome != null && conn != null && !conn.socket.isClosed();
}

public boolean reconnecting() {
return !closed && conn == null;
}

@NotNull
public String deviceId() {
return inner.deviceId;
Expand All @@ -554,6 +559,10 @@ public Random random() {
}

private void reconnect() {
synchronized (reconnectionListeners) {
reconnectionListeners.forEach(ReconnectionListener::onConnectionDropped);
}

try {
if (conn != null) {
conn.socket.close();
Expand All @@ -569,7 +578,12 @@ private void reconnect() {
.build(), true);

LOGGER.info(String.format("Re-authenticated as %s!", apWelcome.getCanonicalUsername()));

synchronized (reconnectionListeners) {
reconnectionListeners.forEach(ReconnectionListener::onConnectionEstablished);
}
} catch (IOException | GeneralSecurityException | SpotifyAuthenticationException ex) {
conn = null;
LOGGER.error("Failed reconnecting, retrying in 10 seconds...", ex);
scheduler.schedule(this::reconnect, 10, TimeUnit.SECONDS);
}
Expand All @@ -589,6 +603,16 @@ public void addCloseListener(@NotNull CloseListener listener) {
if (!closeListeners.contains(listener)) closeListeners.add(listener);
}

public void addReconnectionListener(@NotNull ReconnectionListener listener) {
if (!reconnectionListeners.contains(listener)) reconnectionListeners.add(listener);
}

public interface ReconnectionListener {
void onConnectionDropped();

void onConnectionEstablished();
}

public interface ProxyConfiguration {
boolean proxyEnabled();

Expand Down

0 comments on commit 9c9842a

Please sign in to comment.