Skip to content

Commit 7b6fbb7

Browse files
committed
Added endpoints to retrieve metadata (#149)
1 parent e25fc2f commit 7b6fbb7

9 files changed

Lines changed: 211 additions & 15 deletions

File tree

api/README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,24 @@
33

44
This module depends on `librespot-core` and provides an API to interact with the Spotify client.
55

6-
## How it works
7-
This API uses JSON over Websocket according to the [JSON-RPC 2.0 standard](https://www.jsonrpc.org/specification). Three method prefixes are available:
8-
- `player`, just a placeholder
9-
- `metadata`, allows to retrieve some useful data about tracks and playlist (more to come)
10-
- `mercury`, allows to send requests with Mercury directly, therefore all URIs must start with `hm://`
11-
12-
## Client
13-
You can find a suitable client [here](https://github.com/librespot-org/librespot-java/tree/master/api-client).
6+
## Available endpoints
7+
8+
### Player
9+
- `POST \player\load` Load a track from a given uri. The request body should contain two parameters: `uri` and `play`.
10+
- `POST \player\pause` Pause playback.
11+
- `POST \player\resume` Resume playback.
12+
- `POST \player\next` Skip to next track.
13+
- `POST \player\prev` Skip to previous track.
14+
- `POST \player\set-volume` Set volume to a given `volume` value from 0 to 65536.
15+
- `POST \player\volume-up` Up the volume a little bit.
16+
- `POST \player\volume-down` Lower the volume a little bit.
17+
18+
### Metadata
19+
- `POST \metadata\{type}\{uri}` Retrieve metadata. `type` can be one of `episode`, `track`, `album`, `show`, `artist`, `uri` is the standard Spotify uri.
20+
21+
22+
## Examples
23+
24+
`curl -X POST -d "uri=spotify:track:xxxxxxxxxxxxxxxxxxxxxx&play=true" http://localhost:24879/player/load`
25+
26+
`curl -X POST http://localhost:24879/metadata/track/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.undertow.server.RoutingHandler;
55
import org.apache.log4j.Logger;
66
import org.jetbrains.annotations.NotNull;
7+
import xyz.gianlu.librespot.api.handlers.MetadataHandler;
78
import xyz.gianlu.librespot.api.handlers.PlayerHandler;
89
import xyz.gianlu.librespot.core.Session;
910

@@ -21,7 +22,8 @@ public ApiServer(int port) {
2122
}
2223

2324
private static void prepareHandlers(@NotNull RoutingHandler root, @NotNull Session session) {
24-
root.post("/player/{cmd}", new PlayerHandler(session));
25+
root.post("/player/{cmd}", new PlayerHandler(session))
26+
.post("/metadata/{type}/{uri}", new MetadataHandler(session));
2527
}
2628

2729
public void start(@NotNull Session session) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
public final class Utils {
1616
private static final String INVALID_PARAM_BODY = "{\"name\":\"%s\"}";
17+
private static final String INTERNAL_ERROR_BODY = "{\"msg\":\"%s\"}";
1718
private static final String INVALID_PARAM_WITH_REASON_BODY = "{\"name\":\"%s\",\"reason\":\"%s\"}";
1819

1920
private Utils() {
@@ -57,4 +58,9 @@ public static void invalidParameter(@NotNull HttpServerExchange exchange, @NotNu
5758
exchange.setStatusCode(StatusCodes.BAD_REQUEST);
5859
exchange.getResponseSender().send(String.format(INVALID_PARAM_WITH_REASON_BODY, name, reason));
5960
}
61+
62+
public static void internalError(@NotNull HttpServerExchange exchange, @NotNull Exception ex) {
63+
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
64+
exchange.getResponseSender().send(String.format(INTERNAL_ERROR_BODY, ex.getMessage()));
65+
}
6066
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package xyz.gianlu.librespot.api.handlers;
2+
3+
import com.google.gson.JsonObject;
4+
import io.undertow.server.HttpHandler;
5+
import io.undertow.server.HttpServerExchange;
6+
import org.apache.log4j.Logger;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
import xyz.gianlu.librespot.api.Utils;
10+
import xyz.gianlu.librespot.common.ProtobufToJson;
11+
import xyz.gianlu.librespot.core.Session;
12+
import xyz.gianlu.librespot.dealer.ApiClient;
13+
import xyz.gianlu.librespot.mercury.MercuryClient;
14+
import xyz.gianlu.librespot.mercury.model.*;
15+
16+
import java.io.IOException;
17+
import java.util.Deque;
18+
import java.util.Map;
19+
import java.util.Objects;
20+
21+
/**
22+
* @author Gianlu
23+
*/
24+
public final class MetadataHandler implements HttpHandler {
25+
private static final Logger LOGGER = Logger.getLogger(MetadataHandler.class);
26+
private final Session session;
27+
28+
public MetadataHandler(@NotNull Session session) {
29+
this.session = session;
30+
}
31+
32+
@Override
33+
public void handleRequest(HttpServerExchange exchange) throws Exception {
34+
exchange.startBlocking();
35+
if (exchange.isInIoThread()) {
36+
exchange.dispatch(this);
37+
return;
38+
}
39+
40+
Map<String, Deque<String>> params = Utils.readParameters(exchange);
41+
String typeStr = Utils.getFirstString(params, "type");
42+
if (typeStr == null) {
43+
Utils.invalidParameter(exchange, "type");
44+
return;
45+
}
46+
47+
MetadataType type = MetadataType.parse(typeStr);
48+
if (type == null) {
49+
Utils.invalidParameter(exchange, "type");
50+
return;
51+
}
52+
53+
String uri = Utils.getFirstString(params, "uri");
54+
if (uri == null) {
55+
Utils.invalidParameter(exchange, "uri");
56+
return;
57+
}
58+
59+
try {
60+
JsonObject obj = handle(type, uri);
61+
exchange.getResponseSender().send(obj.toString());
62+
} catch (IOException | MercuryClient.MercuryException ex) {
63+
if (ex instanceof ApiClient.StatusCodeException) {
64+
if (((ApiClient.StatusCodeException) ex).code == 404) {
65+
Utils.invalidParameter(exchange, "uri", "404: Unknown uri");
66+
return;
67+
}
68+
}
69+
70+
Utils.internalError(exchange, ex);
71+
LOGGER.error(String.format("Failed handling api request. {type: %s, uri: %s}", type, uri), ex);
72+
} catch (IllegalArgumentException ex) {
73+
Utils.invalidParameter(exchange, "uri", "Invalid uri for type: " + type);
74+
}
75+
}
76+
77+
@NotNull
78+
private JsonObject handle(@NotNull MetadataType type, @NotNull String uri) throws IOException, MercuryClient.MercuryException, IllegalArgumentException {
79+
switch (type) {
80+
case ALBUM:
81+
return ProtobufToJson.convert(session.api().getMetadata4Album(AlbumId.fromUri(uri)));
82+
case ARTIST:
83+
return ProtobufToJson.convert(session.api().getMetadata4Artist(ArtistId.fromUri(uri)));
84+
case SHOW:
85+
return ProtobufToJson.convert(session.api().getMetadata4Show(ShowId.fromUri(uri)));
86+
case EPISODE:
87+
return ProtobufToJson.convert(session.api().getMetadata4Episode(EpisodeId.fromUri(uri)));
88+
case TRACK:
89+
return ProtobufToJson.convert(session.api().getMetadata4Track(TrackId.fromUri(uri)));
90+
default:
91+
throw new IllegalArgumentException(type.name());
92+
}
93+
}
94+
95+
private enum MetadataType {
96+
EPISODE("episode"), TRACK("track"), ALBUM("album"),
97+
ARTIST("artist"), SHOW("show");
98+
99+
private final String val;
100+
101+
MetadataType(String val) {
102+
this.val = val;
103+
}
104+
105+
@Nullable
106+
private static MetadataType parse(@NotNull String val) {
107+
for (MetadataType type : values())
108+
if (Objects.equals(type.val, val))
109+
return type;
110+
111+
return null;
112+
}
113+
}
114+
}

core/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
import xyz.gianlu.librespot.core.ApResolver;
1212
import xyz.gianlu.librespot.core.Session;
1313
import xyz.gianlu.librespot.mercury.MercuryClient;
14-
import xyz.gianlu.librespot.mercury.model.EpisodeId;
15-
import xyz.gianlu.librespot.mercury.model.TrackId;
14+
import xyz.gianlu.librespot.mercury.model.*;
1615

1716
import java.io.IOException;
1817

@@ -97,20 +96,70 @@ public void putConnectState(@NotNull String connectionId, @NotNull Connect.PutSt
9796
}
9897

9998
@NotNull
100-
public Metadata.Track getMedata4Track(@NotNull TrackId track) throws IOException, MercuryClient.MercuryException {
99+
public Metadata.Track getMetadata4Track(@NotNull TrackId track) throws IOException, MercuryClient.MercuryException {
101100
try (Response resp = send("GET", "/metadata/4/track/" + track.hexId(), null, null)) {
101+
StatusCodeException.checkStatus(resp);
102+
102103
ResponseBody body;
103104
if ((body = resp.body()) == null) throw new IOException();
104105
return Metadata.Track.parseFrom(body.byteStream());
105106
}
106107
}
107108

108109
@NotNull
109-
public Metadata.Episode getMedata4Episode(@NotNull EpisodeId episode) throws IOException, MercuryClient.MercuryException {
110+
public Metadata.Episode getMetadata4Episode(@NotNull EpisodeId episode) throws IOException, MercuryClient.MercuryException {
110111
try (Response resp = send("GET", "/metadata/4/episode/" + episode.hexId(), null, null)) {
112+
StatusCodeException.checkStatus(resp);
113+
111114
ResponseBody body;
112115
if ((body = resp.body()) == null) throw new IOException();
113116
return Metadata.Episode.parseFrom(body.byteStream());
114117
}
115118
}
119+
120+
@NotNull
121+
public Metadata.Album getMetadata4Album(@NotNull AlbumId album) throws IOException, MercuryClient.MercuryException {
122+
try (Response resp = send("GET", "/metadata/4/album/" + album.hexId(), null, null)) {
123+
StatusCodeException.checkStatus(resp);
124+
125+
ResponseBody body;
126+
if ((body = resp.body()) == null) throw new IOException();
127+
return Metadata.Album.parseFrom(body.byteStream());
128+
}
129+
}
130+
131+
@NotNull
132+
public Metadata.Artist getMetadata4Artist(@NotNull ArtistId artist) throws IOException, MercuryClient.MercuryException {
133+
try (Response resp = send("GET", "/metadata/4/artist/" + artist.hexId(), null, null)) {
134+
StatusCodeException.checkStatus(resp);
135+
136+
ResponseBody body;
137+
if ((body = resp.body()) == null) throw new IOException();
138+
return Metadata.Artist.parseFrom(body.byteStream());
139+
}
140+
}
141+
142+
@NotNull
143+
public Metadata.Show getMetadata4Show(@NotNull ShowId show) throws IOException, MercuryClient.MercuryException {
144+
try (Response resp = send("GET", "/metadata/4/show/" + show.hexId(), null, null)) {
145+
StatusCodeException.checkStatus(resp);
146+
147+
ResponseBody body;
148+
if ((body = resp.body()) == null) throw new IOException();
149+
return Metadata.Show.parseFrom(body.byteStream());
150+
}
151+
}
152+
153+
public static class StatusCodeException extends IOException {
154+
public final int code;
155+
156+
StatusCodeException(@NotNull Response resp) {
157+
super(String.format("%d: %s", resp.code(), resp.message()));
158+
code = resp.code();
159+
}
160+
161+
private static void checkStatus(@NotNull Response resp) throws StatusCodeException {
162+
if (resp.code() != 200) throw new StatusCodeException(resp);
163+
}
164+
}
116165
}

core/src/main/java/xyz/gianlu/librespot/mercury/model/AlbumId.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,8 @@ public static AlbumId fromHex(@NotNull String hex) {
4848
public @NotNull String toSpotifyUri() {
4949
return "spotify:album:" + new String(BASE62.encode(Utils.hexToBytes(hexId)));
5050
}
51+
52+
public @NotNull String hexId() {
53+
return hexId;
54+
}
5155
}

core/src/main/java/xyz/gianlu/librespot/mercury/model/ArtistId.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,8 @@ public static ArtistId fromHex(@NotNull String hex) {
4848
public @NotNull String toSpotifyUri() {
4949
return "spotify:artist:" + new String(BASE62.encode(Utils.hexToBytes(hexId)));
5050
}
51+
52+
public @NotNull String hexId() {
53+
return hexId;
54+
}
5155
}

core/src/main/java/xyz/gianlu/librespot/mercury/model/ShowId.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,8 @@ public static ShowId fromHex(@NotNull String hex) {
4848
public @NotNull String toSpotifyUri() {
4949
return "spotify:show:" + new String(BASE62.encode(Utils.hexToBytes(hexId)));
5050
}
51+
52+
public @NotNull String hexId() {
53+
return hexId;
54+
}
5155
}

core/src/main/java/xyz/gianlu/librespot/player/feeders/PlayableContentFeeder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private StorageResolveResponse resolveStorageInteractive(@NotNull ByteString fil
7373
}
7474

7575
private @NotNull LoadedStream loadTrack(@NotNull TrackId id, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, ContentRestrictedException, CdnManager.CdnException {
76-
Metadata.Track original = session.api().getMedata4Track(id);
76+
Metadata.Track original = session.api().getMetadata4Track(id);
7777
Metadata.Track track = pickAlternativeIfNecessary(original);
7878
if (track == null) {
7979
String country = session.countryCode();
@@ -120,7 +120,7 @@ private LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQual
120120

121121
@NotNull
122122
private LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPreference audioQualityPreference, @Nullable AbsChunckedInputStream.HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException {
123-
Metadata.Episode episode = session.api().getMedata4Episode(id);
123+
Metadata.Episode episode = session.api().getMetadata4Episode(id);
124124

125125
if (episode.hasExternalUrl()) {
126126
return CdnFeedHelper.loadEpisodeExternal(session, episode, haltListener);

0 commit comments

Comments
 (0)