diff --git a/api/README.md b/api/README.md index 3e677b54..6f11e7f8 100644 --- a/api/README.md +++ b/api/README.md @@ -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. @@ -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` diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/AbsSessionHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/AbsSessionHandler.java index d2a2c05d..88d6bc13 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/handlers/AbsSessionHandler.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/AbsSessionHandler.java @@ -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; @@ -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); } diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java index 3e68f0e5..fe14be6f 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java @@ -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() { @@ -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); } } diff --git a/core/src/main/java/xyz/gianlu/librespot/core/PacketsManager.java b/core/src/main/java/xyz/gianlu/librespot/core/PacketsManager.java index 14400f65..1b9617ae 100644 --- a/core/src/main/java/xyz/gianlu/librespot/core/PacketsManager.java +++ b/core/src/main/java/xyz/gianlu/librespot/core/PacketsManager.java @@ -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; @@ -66,8 +60,7 @@ public void run() { exception(ex); } }); - } catch (InterruptedException ex) { - executorService.execute(() -> exception(new LooperException(ex))); + } catch (InterruptedException ignored) { } } } diff --git a/core/src/main/java/xyz/gianlu/librespot/core/Session.java b/core/src/main/java/xyz/gianlu/librespot/core/Session.java index 19356905..65e733a6 100644 --- a/core/src/main/java/xyz/gianlu/librespot/core/Session.java +++ b/core/src/main/java/xyz/gianlu/librespot/core/Session.java @@ -76,6 +76,7 @@ public final class Session implements Closeable { private final AtomicBoolean authLock = new AtomicBoolean(false); private final OkHttpClient client; private final List closeListeners = Collections.synchronizedList(new ArrayList<>()); + private final List reconnectionListeners = Collections.synchronizedList(new ArrayList<>()); private ConnectionHolder conn; private CipherPair cipherPair; private Receiver receiver; @@ -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; @@ -554,6 +559,10 @@ public Random random() { } private void reconnect() { + synchronized (reconnectionListeners) { + reconnectionListeners.forEach(ReconnectionListener::onConnectionDropped); + } + try { if (conn != null) { conn.socket.close(); @@ -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); } @@ -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();