diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06080acf..bb1d4ae2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.5.2] - 17-11-2020
+### Added
+- Added `Player#waitReady()` method (d3149d3843e066986524e14369c5871c22629810)
+- Added pass through endpoints for official Spotify API (#255)
+- Store and check hash of first chunk of cache data (9ab9f43a91ebbce0e9a3a3c6f3c55a714c756525)
+
+### Fixed
+- Fixed `UnsupportedOperationException` when starting playback (#251)
+- Close cache files correctly (e953129ed5f0dc4e9931660bd216267557d6010a, #253)
+- Fixed starting playback from API (#254)
+
+
## [1.5.1] - 31-07-2020
### Fixed
- Fixed issue with Zeroconf (#246)
diff --git a/README.md b/README.md
index e62a9204..7ee8f900 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
`librespot-java` is a port of [librespot](https://github.com/librespot-org/librespot), originally written in Rust, which has evolved into the most up-to-date open-source Spotify client. Additionally, this implementation provides a useful API to request metadata or control the player, more [here](api).
## Disclaimer!
-We (the librespot-org organization and me) **DO NOT** encourage piracy and **DO NOT** support any form of downloader/recorder designed with the help of this repository. If you're brave enough to put at risk this entire project, just don't publish it. This is meant to provide support for all those devices that are not officially supported by Spotify.
+We (the librespot-org organization and me) **DO NOT** encourage piracy and **DO NOT** support any form of downloader/recorder designed with the help of this repository and in general anything that goes against the Spotify ToS. If you're brave enough to put at risk this entire project, just don't publish it. This is meant to provide support for all those devices that are not officially supported by Spotify.
## Features
This client is pretty much capable of playing anything that's available on Spotify.
diff --git a/api/README.md b/api/README.md
index 04b131c1..ac95fa43 100644
--- a/api/README.md
+++ b/api/README.md
@@ -57,9 +57,15 @@ The currently available events are:
- `connectionEstablished` Successfully reconnected
- `panic` Entered the panic state, playback is stopped. This is usually recoverable.
+### Web API pass through
+Use any endpoint from the [public Web API](https://developer.spotify.com/documentation/web-api/reference/) by appending it to `/web-api/`, the request will be made to the API with the correct `Authorization` header and the result will be returned.
+The method, body, and content type headers will pass through. Additionally, you can specify an `X-Spotify-Scope` header to override the requested scope, by default all will be requested.
+
## 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`
`curl -X POST http://localhost:24879/metadata/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`
+
+`curl -X GET http://localhost:24879/web-api/v1/me/top/artists`
diff --git a/api/pom.xml b/api/pom.xml
index d0b03128..ba96dcd8 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -5,7 +5,7 @@
xyz.gianlu.librespot
librespot-java
- 1.5.1
+ 1.5.2
../
diff --git a/api/src/main/java/xyz/gianlu/librespot/api/ApiServer.java b/api/src/main/java/xyz/gianlu/librespot/api/ApiServer.java
index 1d5f5952..28eeacfb 100644
--- a/api/src/main/java/xyz/gianlu/librespot/api/ApiServer.java
+++ b/api/src/main/java/xyz/gianlu/librespot/api/ApiServer.java
@@ -2,6 +2,8 @@
import io.undertow.Undertow;
import io.undertow.server.RoutingHandler;
+import io.undertow.server.handlers.PathHandler;
+import io.undertow.server.handlers.ResponseCodeHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
@@ -24,7 +26,10 @@ public ApiServer(int port, @NotNull String host, @NotNull SessionWrapper wrapper
.post("/search/{query}", new SearchHandler(wrapper))
.post("/token/{scope}", new TokensHandler(wrapper))
.post("/profile/{user_id}/{action}", new ProfileHandler(wrapper))
- .get("/events", events);
+ .post("/web-api/{endpoint}", new WebApiHandler(wrapper))
+ .get("/events", events)
+ .setFallbackHandler(new PathHandler(ResponseCodeHandler.HANDLE_404)
+ .addPrefixPath("/web-api", new WebApiHandler(wrapper)));
wrapper.setListener(events);
}
diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java
new file mode 100644
index 00000000..4642831a
--- /dev/null
+++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java
@@ -0,0 +1,64 @@
+package xyz.gianlu.librespot.api.handlers;
+
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.FileUtils;
+import io.undertow.util.HeaderValues;
+import io.undertow.util.Headers;
+import io.undertow.util.HttpString;
+import okhttp3.*;
+import org.jetbrains.annotations.NotNull;
+import xyz.gianlu.librespot.api.SessionWrapper;
+import xyz.gianlu.librespot.core.Session;
+import xyz.gianlu.librespot.core.TokenProvider;
+
+public final class WebApiHandler extends AbsSessionHandler {
+ private static final String[] API_TOKENS_ALL = new String[]{"ugc-image-upload", "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public", "playlist-read-private", "user-read-playback-position", "user-read-recently-played", "user-top-read", "user-modify-playback-state", "user-read-currently-playing", "user-read-playback-state", "user-read-private", "user-read-email", "user-library-modify", "user-library-read", "user-follow-modify", "user-follow-read", "streaming", "app-remote-control"};
+ private static final HttpUrl BASE_API_URL = HttpUrl.get("https://api.spotify.com");
+ private static final HttpString HEADER_X_SCOPE = HttpString.tryFromString("X-Spotify-Scope");
+
+ public WebApiHandler(@NotNull SessionWrapper wrapper) {
+ super(wrapper);
+ }
+
+ @Override
+ protected void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception {
+ exchange.startBlocking();
+ if (exchange.isInIoThread()) {
+ exchange.dispatch(this);
+ return;
+ }
+
+ String body = FileUtils.readFile(exchange.getInputStream());
+ HeaderValues contentType = exchange.getRequestHeaders().get(Headers.CONTENT_TYPE);
+
+ String[] scopes = API_TOKENS_ALL;
+ if (exchange.getRequestHeaders().contains(HEADER_X_SCOPE))
+ scopes = exchange.getRequestHeaders().get(HEADER_X_SCOPE).toArray(new String[0]);
+
+ TokenProvider.StoredToken token = session.tokens().getToken(scopes);
+
+ HttpUrl.Builder url = BASE_API_URL.newBuilder()
+ .addPathSegments(exchange.getRelativePath().substring(1))
+ .query(exchange.getQueryString());
+
+ Request.Builder req = new Request.Builder()
+ .url(url.build())
+ .addHeader("Authorization", "Bearer " + token.accessToken);
+
+ String method = exchange.getRequestMethod().toString();
+ if (!body.isEmpty() && contentType != null)
+ req.method(method, RequestBody.create(body, MediaType.get(contentType.getFirst())));
+ else
+ req.method(method, null);
+
+ try (Response resp = session.client().newCall(req.build()).execute()) {
+ exchange.setStatusCode(resp.code());
+
+ String respContentType = resp.header("Content-Type");
+ if (respContentType != null) exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, respContentType);
+
+ ResponseBody respBody = resp.body();
+ if (respBody != null) exchange.getOutputStream().write(respBody.bytes());
+ }
+ }
+}
diff --git a/lib/pom.xml b/lib/pom.xml
index 33330e63..9e864173 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -5,7 +5,7 @@
xyz.gianlu.librespot
librespot-java
- 1.5.1
+ 1.5.2
../
@@ -97,7 +97,7 @@
commons-net
commons-net
- 3.6
+ 3.7.2
\ No newline at end of file
diff --git a/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java b/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java
index 3df431dd..51ddf514 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java
@@ -206,7 +206,6 @@ private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @
boolean fromCache;
byte[] firstChunk;
byte[] sizeHeader;
-
if (cacheHandler != null && (sizeHeader = cacheHandler.getHeader(AudioFileFetch.HEADER_SIZE)) != null) {
size = ByteBuffer.wrap(sizeHeader).getInt() * 4;
chunks = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
@@ -214,7 +213,7 @@ private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @
try {
firstChunk = cacheHandler.readChunk(0);
fromCache = true;
- } catch (IOException ex) {
+ } catch (IOException | CacheManager.BadChunkHashException ex) {
LOGGER.error("Failed getting first chunk from cache.", ex);
InternalResponse resp = request(0, CHUNK_SIZE - 1);
@@ -294,7 +293,7 @@ private void requestChunk(int index) {
cacheHandler.readChunk(index, this);
return;
}
- } catch (IOException ex) {
+ } catch (IOException | CacheManager.BadChunkHashException ex) {
LOGGER.fatal("Failed requesting chunk from cache, index: {}", index, ex);
}
}
@@ -344,6 +343,13 @@ private InternalStream(boolean retryOnChunkError) {
public void close() {
super.close();
executorService.shutdown();
+
+ if (cacheHandler != null) {
+ try {
+ cacheHandler.close();
+ } catch (IOException ignored) {
+ }
+ }
}
@Override
diff --git a/lib/src/main/java/xyz/gianlu/librespot/audio/storage/AudioFileStreaming.java b/lib/src/main/java/xyz/gianlu/librespot/audio/storage/AudioFileStreaming.java
index dc5908b8..cbdee021 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/audio/storage/AudioFileStreaming.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/audio/storage/AudioFileStreaming.java
@@ -83,7 +83,7 @@ private boolean tryCacheChunk(int index) {
if (!cacheHandler.hasChunk(index)) return false;
cacheHandler.readChunk(index, this);
return true;
- } catch (IOException ex) {
+ } catch (IOException | CacheManager.BadChunkHashException ex) {
LOGGER.fatal("Failed requesting chunk from cache, index: {}", index, ex);
return false;
}
@@ -156,6 +156,13 @@ public void close() {
executorService.shutdown();
if (chunksBuffer != null)
chunksBuffer.close();
+
+ if (cacheHandler != null) {
+ try {
+ cacheHandler.close();
+ } catch (IOException ignored) {
+ }
+ }
}
private class ChunksBuffer implements Closeable {
@@ -194,6 +201,7 @@ AbsChunkedInputStream stream() {
@Override
public void close() {
internalStream.close();
+ AudioFileStreaming.this.close();
}
private class InternalStream extends AbsChunkedInputStream {
diff --git a/lib/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java b/lib/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java
index 776426bd..fdf93e27 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java
@@ -6,6 +6,7 @@
import org.jetbrains.annotations.Nullable;
import xyz.gianlu.librespot.audio.GeneralWritableStream;
import xyz.gianlu.librespot.audio.StreamId;
+import xyz.gianlu.librespot.common.Utils;
import xyz.gianlu.librespot.core.Session;
import java.io.Closeable;
@@ -13,10 +14,9 @@
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -29,7 +29,14 @@
public class CacheManager implements Closeable {
private static final long CLEAN_UP_THRESHOLD = TimeUnit.DAYS.toMillis(7);
private static final Logger LOGGER = LogManager.getLogger(CacheManager.class);
+ /**
+ * The header indicating when the file was last read or written to.
+ */
private static final int HEADER_TIMESTAMP = 254;
+ /**
+ * The header indicating the hash of the first chunk of the file.
+ */
+ private static final int HEADER_HASH = 253;
private final File parent;
private final CacheJournal journal;
private final Map fileHandlers = new ConcurrentHashMap<>();
@@ -127,6 +134,13 @@ public Handler getHandler(@NotNull StreamId streamId) throws IOException {
return getHandler(streamId.isEpisode() ? streamId.getEpisodeGid() : streamId.getFileId());
}
+ public static class BadChunkHashException extends Exception {
+ BadChunkHashException(@NotNull String streamId, byte[] expected, byte[] actual) {
+ super(String.format("Failed verifying chunk hash for %s, expected: %s, actual: %s",
+ streamId, Utils.bytesToHex(expected), Utils.bytesToHex(actual)));
+ }
+ }
+
public class Handler implements Closeable {
private final String streamId;
private final RandomAccessFile io;
@@ -173,21 +187,35 @@ public byte[] getHeader(byte id) throws IOException {
return header == null ? null : header.value;
}
+ /**
+ * Checks if the chunk is present in the cache, WITHOUT checking the hash.
+ *
+ * @param index The index of the chunk
+ * @return Whether the chunk is available
+ */
public boolean hasChunk(int index) throws IOException {
updateTimestamp();
synchronized (io) {
- if (io.length() < (index + 1) * CHUNK_SIZE) return false;
+ if (io.length() < (index + 1) * CHUNK_SIZE)
+ return false;
}
return journal.hasChunk(streamId, index);
}
- public void readChunk(int index, @NotNull GeneralWritableStream stream) throws IOException {
+ public void readChunk(int index, @NotNull GeneralWritableStream stream) throws IOException, BadChunkHashException {
stream.writeChunk(readChunk(index), index, true);
}
- public byte[] readChunk(int index) throws IOException {
+ /**
+ * Reads the given chunk.
+ *
+ * @param index The index of the chunk
+ * @return The buffer containing the content of the chunk
+ * @throws BadChunkHashException If {@code index == 0} and the hash doesn't match
+ */
+ public byte[] readChunk(int index) throws IOException, BadChunkHashException {
updateTimestamp();
synchronized (io) {
@@ -198,6 +226,22 @@ public byte[] readChunk(int index) throws IOException {
if (read != buffer.length)
throw new IOException(String.format("Couldn't read full chunk, read: %d, needed: %d", read, buffer.length));
+ if (index == 0) {
+ JournalHeader header = journal.getHeader(streamId, HEADER_HASH);
+ if (header != null) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+ byte[] hash = digest.digest(buffer);
+ if (!Arrays.equals(header.value, hash)) {
+ journal.setChunk(streamId, index, false);
+ throw new BadChunkHashException(streamId, header.value, hash);
+ }
+ } catch (NoSuchAlgorithmException ex) {
+ LOGGER.error("Failed initializing MD5 digest.", ex);
+ }
+ }
+ }
+
return buffer;
}
}
@@ -210,6 +254,16 @@ public void writeChunk(byte[] buffer, int index) throws IOException {
try {
journal.setChunk(streamId, index, true);
+
+ if (index == 0) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+ byte[] hash = digest.digest(buffer);
+ journal.setHeader(streamId, HEADER_HASH, hash);
+ } catch (NoSuchAlgorithmException ex) {
+ LOGGER.error("Failed initializing MD5 digest.", ex);
+ }
+ }
} finally {
updateTimestamp();
}
diff --git a/lib/src/main/java/xyz/gianlu/librespot/common/ProtoUtils.java b/lib/src/main/java/xyz/gianlu/librespot/common/ProtoUtils.java
index a77d1f1a..653e36a1 100644
--- a/lib/src/main/java/xyz/gianlu/librespot/common/ProtoUtils.java
+++ b/lib/src/main/java/xyz/gianlu/librespot/common/ProtoUtils.java
@@ -181,12 +181,14 @@ public static Player.PlayOrigin convertPlayOrigin(@Nullable PlayOrigin po) {
@NotNull
- public static ContextPlayerOptions jsonToPlayerOptions(@NotNull JsonObject obj, @Nullable ContextPlayerOptions old) {
+ public static ContextPlayerOptions jsonToPlayerOptions(@Nullable JsonObject obj, @Nullable ContextPlayerOptions old) {
ContextPlayerOptions.Builder builder = old == null ? ContextPlayerOptions.newBuilder() : old.toBuilder();
- Optional.ofNullable(obj.get("repeating_context")).ifPresent(elm -> builder.setRepeatingContext(elm.getAsBoolean()));
- Optional.ofNullable(obj.get("repeating_track")).ifPresent(elm -> builder.setRepeatingTrack(elm.getAsBoolean()));
- Optional.ofNullable(obj.get("shuffling_context")).ifPresent(elm -> builder.setShufflingContext(elm.getAsBoolean()));
+ if (obj != null) {
+ Optional.ofNullable(obj.get("repeating_context")).ifPresent(elm -> builder.setRepeatingContext(elm.getAsBoolean()));
+ Optional.ofNullable(obj.get("repeating_track")).ifPresent(elm -> builder.setRepeatingTrack(elm.getAsBoolean()));
+ Optional.ofNullable(obj.get("shuffling_context")).ifPresent(elm -> builder.setShufflingContext(elm.getAsBoolean()));
+ }
return builder.build();
}
diff --git a/lib/src/main/test/xyz/gianlu/librespot/SpotifyUrisTest.java b/lib/src/main/test/java/xyz/gianlu/librespot/SpotifyUrisTest.java
similarity index 100%
rename from lib/src/main/test/xyz/gianlu/librespot/SpotifyUrisTest.java
rename to lib/src/main/test/java/xyz/gianlu/librespot/SpotifyUrisTest.java
diff --git a/lib/src/main/test/xyz/gianlu/librespot/cache/CacheTest.java b/lib/src/main/test/java/xyz/gianlu/librespot/cache/CacheTest.java
similarity index 100%
rename from lib/src/main/test/xyz/gianlu/librespot/cache/CacheTest.java
rename to lib/src/main/test/java/xyz/gianlu/librespot/cache/CacheTest.java
diff --git a/lib/src/main/test/xyz/gianlu/librespot/common/AsyncProcessorTest.java b/lib/src/main/test/java/xyz/gianlu/librespot/common/AsyncProcessorTest.java
similarity index 100%
rename from lib/src/main/test/xyz/gianlu/librespot/common/AsyncProcessorTest.java
rename to lib/src/main/test/java/xyz/gianlu/librespot/common/AsyncProcessorTest.java
diff --git a/lib/src/main/test/xyz/gianlu/librespot/common/FisherYatesTest.java b/lib/src/main/test/java/xyz/gianlu/librespot/common/FisherYatesTest.java
similarity index 100%
rename from lib/src/main/test/xyz/gianlu/librespot/common/FisherYatesTest.java
rename to lib/src/main/test/java/xyz/gianlu/librespot/common/FisherYatesTest.java
diff --git a/player/pom.xml b/player/pom.xml
index 8f32c11f..89152175 100644
--- a/player/pom.xml
+++ b/player/pom.xml
@@ -5,7 +5,7 @@
xyz.gianlu.librespot
librespot-java
- 1.5.1
+ 1.5.2
../
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/Player.java b/player/src/main/java/xyz/gianlu/librespot/player/Player.java
index a06b42e5..f9d46b26 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/Player.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/Player.java
@@ -159,6 +159,30 @@ public void removeFromQueue(@NotNull String uri) {
state.updated();
}
+ @NotNull
+ public Future waitReady() {
+ CompletableFuture future = new CompletableFuture<>();
+ state.addListener(new DeviceStateHandler.Listener() {
+ @Override
+ public void ready() {
+ future.complete(Player.this);
+ }
+
+ @Override
+ public void command(DeviceStateHandler.@NotNull Endpoint endpoint, DeviceStateHandler.@NotNull CommandBody data) throws InvalidProtocolBufferException {
+ }
+
+ @Override
+ public void volumeChanged() {
+ }
+
+ @Override
+ public void notActive() {
+ }
+ });
+ return future;
+ }
+
// ================================ //
// ======== Internal state ======== //
@@ -315,7 +339,7 @@ private void handlePlay(@NotNull JsonObject obj) {
events.contextChanged();
Boolean paused = PlayCommandHelper.isInitiallyPaused(obj);
- if (paused == null) paused = true;
+ if (paused == null) paused = false;
loadSession(sessionId, !paused, PlayCommandHelper.willSkipToSomething(obj));
} catch (IOException | MercuryClient.MercuryException ex) {
LOGGER.fatal("Failed loading context!", ex);
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/codecs/VorbisCodec.java b/player/src/main/java/xyz/gianlu/librespot/player/codecs/VorbisCodec.java
index 84bd796f..a9575af4 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/codecs/VorbisCodec.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/codecs/VorbisCodec.java
@@ -223,7 +223,7 @@ public void close() throws IOException {
joggStreamState.clear();
jorbisBlock.clear();
jorbisDspState.clear();
- jorbisInfo.clear();
+ // jorbisInfo.clear();
joggSyncState.clear();
}
}
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java
index 5a44829e..f3b5d83e 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java
@@ -352,6 +352,11 @@ boolean closeIfUseless() {
public void close() {
closed = true;
clearOutput();
+
+ try {
+ if (codec != null) codec.close();
+ } catch (IOException ignored) {
+ }
}
@Override
diff --git a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
index b6b193d7..4f5c8211 100644
--- a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
+++ b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java
@@ -323,7 +323,7 @@ public static JsonObject getContext(@NotNull JsonObject obj) {
return obj.getAsJsonObject("context");
}
- @NotNull
+ @Nullable
public static JsonObject getPlayerOptionsOverride(@NotNull JsonObject obj) {
return obj.getAsJsonObject("options").getAsJsonObject("player_options_override");
}
diff --git a/pom.xml b/pom.xml
index 138e65c2..0991cf96 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
xyz.gianlu.librespot
librespot-java
pom
- 1.5.1
+ 1.5.2
librespot-java
Java port of librespot, the Open Source Spotify client library
@@ -38,7 +38,7 @@
1.8
1.8
2.8.6
- 3.12.2
+ 3.14.0
@@ -52,7 +52,7 @@
org.jetbrains
annotations
- 19.0.0
+ 20.1.0
@@ -78,7 +78,7 @@
org.junit.jupiter
junit-jupiter
- 5.6.2
+ 5.7.0
test
@@ -141,7 +141,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.1.1
+ 3.2.0
attach-javadocs