Skip to content

Commit a7eceb0

Browse files
devgianlujordi735dependabot-preview[bot]
authored
Release 1.5.2 (#261)
* [ci skip] Bump to snapshot version * Added #waitReady() method to avoid using player before initialization * Updated disclaimer * Make sure that cache files and queue entries are closed * Fixed NPE * Added cacheHandler checking (#253) + Added check in CdnManager.java + Added check in AudioFileStreaming.java * Fixed NPE when skipping before song loaded * Added pass through endpoint to API (#255) * Handle null `player_options_override` (#254) * Do not pause if not specified (#254) * Fixed tests path * Added hash check for first chunk of cached data * Bump commons-net from 3.6 to 3.7.2 (#256) Bumps commons-net from 3.6 to 3.7.2. Signed-off-by: dependabot-preview[bot] <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> * Bump annotations from 19.0.0 to 20.1.0 (#257) Bumps [annotations](https://github.com/JetBrains/java-annotations) from 19.0.0 to 20.1.0. - [Release notes](https://github.com/JetBrains/java-annotations/releases) - [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md) - [Commits](JetBrains/java-annotations@19.0.0...20.1.0) Signed-off-by: dependabot-preview[bot] <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> * Bump protobuf-java from 3.12.2 to 3.14.0 (#258) Bumps [protobuf-java](https://github.com/protocolbuffers/protobuf) from 3.12.2 to 3.14.0. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py) - [Commits](protocolbuffers/protobuf@v3.12.2...v3.14.0) Signed-off-by: dependabot-preview[bot] <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> * Bump junit-jupiter from 5.6.2 to 5.7.0 (#259) Bumps [junit-jupiter](https://github.com/junit-team/junit5) from 5.6.2 to 5.7.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](junit-team/junit-framework@r5.6.2...r5.7.0) Signed-off-by: dependabot-preview[bot] <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> * Bump maven-javadoc-plugin from 3.1.1 to 3.2.0 (#260) Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](apache/maven-javadoc-plugin@maven-javadoc-plugin-3.1.1...maven-javadoc-plugin-3.2.0) Signed-off-by: dependabot-preview[bot] <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> * Release 1.5.2 Co-authored-by: Jordi735 <[email protected]> Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
1 parent d69840b commit a7eceb0

21 files changed

Lines changed: 215 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.5.2] - 17-11-2020
8+
### Added
9+
- Added `Player#waitReady()` method (d3149d3843e066986524e14369c5871c22629810)
10+
- Added pass through endpoints for official Spotify API (#255)
11+
- Store and check hash of first chunk of cache data (9ab9f43a91ebbce0e9a3a3c6f3c55a714c756525)
12+
13+
### Fixed
14+
- Fixed `UnsupportedOperationException` when starting playback (#251)
15+
- Close cache files correctly (e953129ed5f0dc4e9931660bd216267557d6010a, #253)
16+
- Fixed starting playback from API (#254)
17+
18+
719
## [1.5.1] - 31-07-2020
820
### Fixed
921
- Fixed issue with Zeroconf (#246)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
`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).
88

99
## Disclaimer!
10-
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.
10+
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.
1111

1212
## Features
1313
This client is pretty much capable of playing anything that's available on Spotify.

api/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@ The currently available events are:
5757
- `connectionEstablished` Successfully reconnected
5858
- `panic` Entered the panic state, playback is stopped. This is usually recoverable.
5959

60+
### Web API pass through
61+
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.
62+
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.
63+
6064
## Examples
6165
`curl -X POST -d "uri=spotify:track:xxxxxxxxxxxxxxxxxxxxxx&play=true" http://localhost:24879/player/load`
6266

6367
`curl -X POST http://localhost:24879/metadata/track/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`
6468

6569
`curl -X POST http://localhost:24879/metadata/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`
70+
71+
`curl -X GET http://localhost:24879/web-api/v1/me/top/artists`

api/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>xyz.gianlu.librespot</groupId>
77
<artifactId>librespot-java</artifactId>
8-
<version>1.5.1</version>
8+
<version>1.5.2</version>
99
<relativePath>../</relativePath>
1010
</parent>
1111

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import io.undertow.Undertow;
44
import io.undertow.server.RoutingHandler;
5+
import io.undertow.server.handlers.PathHandler;
6+
import io.undertow.server.handlers.ResponseCodeHandler;
57
import org.apache.logging.log4j.LogManager;
68
import org.apache.logging.log4j.Logger;
79
import org.jetbrains.annotations.NotNull;
@@ -24,7 +26,10 @@ public ApiServer(int port, @NotNull String host, @NotNull SessionWrapper wrapper
2426
.post("/search/{query}", new SearchHandler(wrapper))
2527
.post("/token/{scope}", new TokensHandler(wrapper))
2628
.post("/profile/{user_id}/{action}", new ProfileHandler(wrapper))
27-
.get("/events", events);
29+
.post("/web-api/{endpoint}", new WebApiHandler(wrapper))
30+
.get("/events", events)
31+
.setFallbackHandler(new PathHandler(ResponseCodeHandler.HANDLE_404)
32+
.addPrefixPath("/web-api", new WebApiHandler(wrapper)));
2833

2934
wrapper.setListener(events);
3035
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package xyz.gianlu.librespot.api.handlers;
2+
3+
import io.undertow.server.HttpServerExchange;
4+
import io.undertow.util.FileUtils;
5+
import io.undertow.util.HeaderValues;
6+
import io.undertow.util.Headers;
7+
import io.undertow.util.HttpString;
8+
import okhttp3.*;
9+
import org.jetbrains.annotations.NotNull;
10+
import xyz.gianlu.librespot.api.SessionWrapper;
11+
import xyz.gianlu.librespot.core.Session;
12+
import xyz.gianlu.librespot.core.TokenProvider;
13+
14+
public final class WebApiHandler extends AbsSessionHandler {
15+
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"};
16+
private static final HttpUrl BASE_API_URL = HttpUrl.get("https://api.spotify.com");
17+
private static final HttpString HEADER_X_SCOPE = HttpString.tryFromString("X-Spotify-Scope");
18+
19+
public WebApiHandler(@NotNull SessionWrapper wrapper) {
20+
super(wrapper);
21+
}
22+
23+
@Override
24+
protected void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception {
25+
exchange.startBlocking();
26+
if (exchange.isInIoThread()) {
27+
exchange.dispatch(this);
28+
return;
29+
}
30+
31+
String body = FileUtils.readFile(exchange.getInputStream());
32+
HeaderValues contentType = exchange.getRequestHeaders().get(Headers.CONTENT_TYPE);
33+
34+
String[] scopes = API_TOKENS_ALL;
35+
if (exchange.getRequestHeaders().contains(HEADER_X_SCOPE))
36+
scopes = exchange.getRequestHeaders().get(HEADER_X_SCOPE).toArray(new String[0]);
37+
38+
TokenProvider.StoredToken token = session.tokens().getToken(scopes);
39+
40+
HttpUrl.Builder url = BASE_API_URL.newBuilder()
41+
.addPathSegments(exchange.getRelativePath().substring(1))
42+
.query(exchange.getQueryString());
43+
44+
Request.Builder req = new Request.Builder()
45+
.url(url.build())
46+
.addHeader("Authorization", "Bearer " + token.accessToken);
47+
48+
String method = exchange.getRequestMethod().toString();
49+
if (!body.isEmpty() && contentType != null)
50+
req.method(method, RequestBody.create(body, MediaType.get(contentType.getFirst())));
51+
else
52+
req.method(method, null);
53+
54+
try (Response resp = session.client().newCall(req.build()).execute()) {
55+
exchange.setStatusCode(resp.code());
56+
57+
String respContentType = resp.header("Content-Type");
58+
if (respContentType != null) exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, respContentType);
59+
60+
ResponseBody respBody = resp.body();
61+
if (respBody != null) exchange.getOutputStream().write(respBody.bytes());
62+
}
63+
}
64+
}

lib/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>xyz.gianlu.librespot</groupId>
77
<artifactId>librespot-java</artifactId>
8-
<version>1.5.1</version>
8+
<version>1.5.2</version>
99
<relativePath>../</relativePath>
1010
</parent>
1111

@@ -97,7 +97,7 @@
9797
<dependency>
9898
<groupId>commons-net</groupId>
9999
<artifactId>commons-net</artifactId>
100-
<version>3.6</version>
100+
<version>3.7.2</version>
101101
</dependency>
102102
</dependencies>
103103
</project>

lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,14 @@ private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @
206206
boolean fromCache;
207207
byte[] firstChunk;
208208
byte[] sizeHeader;
209-
210209
if (cacheHandler != null && (sizeHeader = cacheHandler.getHeader(AudioFileFetch.HEADER_SIZE)) != null) {
211210
size = ByteBuffer.wrap(sizeHeader).getInt() * 4;
212211
chunks = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
213212

214213
try {
215214
firstChunk = cacheHandler.readChunk(0);
216215
fromCache = true;
217-
} catch (IOException ex) {
216+
} catch (IOException | CacheManager.BadChunkHashException ex) {
218217
LOGGER.error("Failed getting first chunk from cache.", ex);
219218

220219
InternalResponse resp = request(0, CHUNK_SIZE - 1);
@@ -294,7 +293,7 @@ private void requestChunk(int index) {
294293
cacheHandler.readChunk(index, this);
295294
return;
296295
}
297-
} catch (IOException ex) {
296+
} catch (IOException | CacheManager.BadChunkHashException ex) {
298297
LOGGER.fatal("Failed requesting chunk from cache, index: {}", index, ex);
299298
}
300299
}
@@ -344,6 +343,13 @@ private InternalStream(boolean retryOnChunkError) {
344343
public void close() {
345344
super.close();
346345
executorService.shutdown();
346+
347+
if (cacheHandler != null) {
348+
try {
349+
cacheHandler.close();
350+
} catch (IOException ignored) {
351+
}
352+
}
347353
}
348354

349355
@Override

lib/src/main/java/xyz/gianlu/librespot/audio/storage/AudioFileStreaming.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private boolean tryCacheChunk(int index) {
8383
if (!cacheHandler.hasChunk(index)) return false;
8484
cacheHandler.readChunk(index, this);
8585
return true;
86-
} catch (IOException ex) {
86+
} catch (IOException | CacheManager.BadChunkHashException ex) {
8787
LOGGER.fatal("Failed requesting chunk from cache, index: {}", index, ex);
8888
return false;
8989
}
@@ -156,6 +156,13 @@ public void close() {
156156
executorService.shutdown();
157157
if (chunksBuffer != null)
158158
chunksBuffer.close();
159+
160+
if (cacheHandler != null) {
161+
try {
162+
cacheHandler.close();
163+
} catch (IOException ignored) {
164+
}
165+
}
159166
}
160167

161168
private class ChunksBuffer implements Closeable {
@@ -194,6 +201,7 @@ AbsChunkedInputStream stream() {
194201
@Override
195202
public void close() {
196203
internalStream.close();
204+
AudioFileStreaming.this.close();
197205
}
198206

199207
private class InternalStream extends AbsChunkedInputStream {

lib/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
import org.jetbrains.annotations.Nullable;
77
import xyz.gianlu.librespot.audio.GeneralWritableStream;
88
import xyz.gianlu.librespot.audio.StreamId;
9+
import xyz.gianlu.librespot.common.Utils;
910
import xyz.gianlu.librespot.core.Session;
1011

1112
import java.io.Closeable;
1213
import java.io.File;
1314
import java.io.IOException;
1415
import java.io.RandomAccessFile;
1516
import java.math.BigInteger;
16-
import java.util.ArrayList;
17-
import java.util.Iterator;
18-
import java.util.List;
19-
import java.util.Map;
17+
import java.security.MessageDigest;
18+
import java.security.NoSuchAlgorithmException;
19+
import java.util.*;
2020
import java.util.concurrent.ConcurrentHashMap;
2121
import java.util.concurrent.TimeUnit;
2222

@@ -29,7 +29,14 @@
2929
public class CacheManager implements Closeable {
3030
private static final long CLEAN_UP_THRESHOLD = TimeUnit.DAYS.toMillis(7);
3131
private static final Logger LOGGER = LogManager.getLogger(CacheManager.class);
32+
/**
33+
* The header indicating when the file was last read or written to.
34+
*/
3235
private static final int HEADER_TIMESTAMP = 254;
36+
/**
37+
* The header indicating the hash of the first chunk of the file.
38+
*/
39+
private static final int HEADER_HASH = 253;
3340
private final File parent;
3441
private final CacheJournal journal;
3542
private final Map<String, Handler> fileHandlers = new ConcurrentHashMap<>();
@@ -127,6 +134,13 @@ public Handler getHandler(@NotNull StreamId streamId) throws IOException {
127134
return getHandler(streamId.isEpisode() ? streamId.getEpisodeGid() : streamId.getFileId());
128135
}
129136

137+
public static class BadChunkHashException extends Exception {
138+
BadChunkHashException(@NotNull String streamId, byte[] expected, byte[] actual) {
139+
super(String.format("Failed verifying chunk hash for %s, expected: %s, actual: %s",
140+
streamId, Utils.bytesToHex(expected), Utils.bytesToHex(actual)));
141+
}
142+
}
143+
130144
public class Handler implements Closeable {
131145
private final String streamId;
132146
private final RandomAccessFile io;
@@ -173,21 +187,35 @@ public byte[] getHeader(byte id) throws IOException {
173187
return header == null ? null : header.value;
174188
}
175189

190+
/**
191+
* Checks if the chunk is present in the cache, WITHOUT checking the hash.
192+
*
193+
* @param index The index of the chunk
194+
* @return Whether the chunk is available
195+
*/
176196
public boolean hasChunk(int index) throws IOException {
177197
updateTimestamp();
178198

179199
synchronized (io) {
180-
if (io.length() < (index + 1) * CHUNK_SIZE) return false;
200+
if (io.length() < (index + 1) * CHUNK_SIZE)
201+
return false;
181202
}
182203

183204
return journal.hasChunk(streamId, index);
184205
}
185206

186-
public void readChunk(int index, @NotNull GeneralWritableStream stream) throws IOException {
207+
public void readChunk(int index, @NotNull GeneralWritableStream stream) throws IOException, BadChunkHashException {
187208
stream.writeChunk(readChunk(index), index, true);
188209
}
189210

190-
public byte[] readChunk(int index) throws IOException {
211+
/**
212+
* Reads the given chunk.
213+
*
214+
* @param index The index of the chunk
215+
* @return The buffer containing the content of the chunk
216+
* @throws BadChunkHashException If {@code index == 0} and the hash doesn't match
217+
*/
218+
public byte[] readChunk(int index) throws IOException, BadChunkHashException {
191219
updateTimestamp();
192220

193221
synchronized (io) {
@@ -198,6 +226,22 @@ public byte[] readChunk(int index) throws IOException {
198226
if (read != buffer.length)
199227
throw new IOException(String.format("Couldn't read full chunk, read: %d, needed: %d", read, buffer.length));
200228

229+
if (index == 0) {
230+
JournalHeader header = journal.getHeader(streamId, HEADER_HASH);
231+
if (header != null) {
232+
try {
233+
MessageDigest digest = MessageDigest.getInstance("MD5");
234+
byte[] hash = digest.digest(buffer);
235+
if (!Arrays.equals(header.value, hash)) {
236+
journal.setChunk(streamId, index, false);
237+
throw new BadChunkHashException(streamId, header.value, hash);
238+
}
239+
} catch (NoSuchAlgorithmException ex) {
240+
LOGGER.error("Failed initializing MD5 digest.", ex);
241+
}
242+
}
243+
}
244+
201245
return buffer;
202246
}
203247
}
@@ -210,6 +254,16 @@ public void writeChunk(byte[] buffer, int index) throws IOException {
210254

211255
try {
212256
journal.setChunk(streamId, index, true);
257+
258+
if (index == 0) {
259+
try {
260+
MessageDigest digest = MessageDigest.getInstance("MD5");
261+
byte[] hash = digest.digest(buffer);
262+
journal.setHeader(streamId, HEADER_HASH, hash);
263+
} catch (NoSuchAlgorithmException ex) {
264+
LOGGER.error("Failed initializing MD5 digest.", ex);
265+
}
266+
}
213267
} finally {
214268
updateTimestamp();
215269
}

0 commit comments

Comments
 (0)