Skip to content

Commit ad1702f

Browse files
committed
Added pass through endpoint to API (#255)
1 parent cdbd1d7 commit ad1702f

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

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/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+
}

0 commit comments

Comments
 (0)