Skip to content

Commit de58e15

Browse files
authored
Merge pull request #29 from librespot-org/api-development
API development 1
2 parents 7ce928c + 868ad16 commit de58e15

16 files changed

Lines changed: 335 additions & 27 deletions

File tree

api-client/src/main/java/xyz.gianlu.librespot.api.client/MainController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,31 @@ public void clickedResponse(MouseEvent mouseEvent) throws IOException {
201201
stage.showAndWait();
202202
}
203203

204+
public void clickedPlayerPlay(MouseEvent event) {
205+
if (networkThread == null) return;
206+
networkThread.sendPlayer("play", this);
207+
}
208+
209+
public void clickedPlayerPause(MouseEvent event) {
210+
if (networkThread == null) return;
211+
networkThread.sendPlayer("pause", this);
212+
}
213+
214+
public void clickedPlayerPlayPause(MouseEvent event) {
215+
if (networkThread == null) return;
216+
networkThread.sendPlayer("playPause", this);
217+
}
218+
219+
public void clickedPlayerNext(MouseEvent event) {
220+
if (networkThread == null) return;
221+
networkThread.sendPlayer("next", this);
222+
}
223+
224+
public void clickedPlayerPrev(MouseEvent event) {
225+
if (networkThread == null) return;
226+
networkThread.sendPlayer("prev", this);
227+
}
228+
204229
public static class Header {
205230
private final SimpleStringProperty key;
206231
private final SimpleStringProperty value;

api-client/src/main/java/xyz.gianlu.librespot.api.client/NetworkThread.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ void sendGeneral(@NotNull String id, @NotNull String method, @Nullable String pa
6565
requests.put(id, listener);
6666
}
6767

68+
void sendPlayer(@NotNull String suffix, @NotNull Callback listener) {
69+
sendGeneral(String.valueOf(ThreadLocalRandom.current().nextInt(1000)), "player." + suffix, null, listener);
70+
}
71+
6872
void sendMercury(@NotNull String method, @NotNull String uri, @Nullable String contentType, @NotNull Map<String, String> headers, @NotNull Callback listener) {
6973
String id = String.valueOf(ThreadLocalRandom.current().nextInt(1000));
7074

api-client/src/main/resources/main.fxml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,42 @@
133133
</HBox>
134134
</content>
135135
</Tab>
136+
<Tab closable="false" text="Player">
137+
<content>
138+
<FlowPane>
139+
<children>
140+
<Button mnemonicParsing="false" onMouseClicked="#clickedPlayerPlay" text="Play">
141+
<FlowPane.margin>
142+
<Insets right="8.0"/>
143+
</FlowPane.margin>
144+
</Button>
145+
<Button layoutX="18.0" layoutY="18.0" onMouseClicked="#clickedPlayerPause"
146+
mnemonicParsing="false" text="Pause">
147+
<FlowPane.margin>
148+
<Insets right="8.0"/>
149+
</FlowPane.margin>
150+
</Button>
151+
<Button layoutX="132.0" layoutY="18.0" onMouseClicked="#clickedPlayerPlayPause"
152+
mnemonicParsing="false" text="Play/Pause">
153+
<FlowPane.margin>
154+
<Insets right="8.0"/>
155+
</FlowPane.margin>
156+
</Button>
157+
<Button layoutX="56.0" layoutY="18.0" onMouseClicked="#clickedPlayerNext"
158+
mnemonicParsing="false" text="Next">
159+
<FlowPane.margin>
160+
<Insets right="8.0"/>
161+
</FlowPane.margin>
162+
</Button>
163+
<Button layoutX="56.0" layoutY="18.0" onMouseClicked="#clickedPlayerPrev"
164+
mnemonicParsing="false" text="Previous"/>
165+
</children>
166+
<padding>
167+
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0"/>
168+
</padding>
169+
</FlowPane>
170+
</content>
171+
</Tab>
136172
</tabs>
137173
</TabPane>
138174
</content>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static void main(String[] args) throws IOException, GeneralSecurityExcept
2121
.create();
2222

2323
ApiServer server = new ApiServer(24879);
24-
server.registerHandler(new PlayerHandler());
24+
server.registerHandler(new PlayerHandler(session));
2525
server.registerHandler(new MetadataHandler(session));
2626
server.registerHandler(new MercuryHandler(session));
2727
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public MercuryHandler(@NotNull Session session) {
5454
obj.add("payloads", payloads);
5555
return obj;
5656
} catch (IOException ex) {
57-
throw new HandlingException(ex, 100 /* FIXME */);
57+
throw new HandlingException(ex, ErrorCode.IO_EXCEPTION);
5858
}
5959
} else {
6060
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.METHOD_NOT_FOUND);

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package xyz.gianlu.librespot.api;
22

33
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonObject;
45
import com.google.protobuf.AbstractMessageLite;
56
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
68
import xyz.gianlu.librespot.api.server.AbsApiHandler;
79
import xyz.gianlu.librespot.api.server.ApiServer;
810
import xyz.gianlu.librespot.core.Session;
911
import xyz.gianlu.librespot.mercury.MercuryClient;
1012
import xyz.gianlu.librespot.mercury.MercuryRequests;
1113
import xyz.gianlu.librespot.mercury.ProtoJsonMercuryRequest;
12-
import xyz.gianlu.librespot.mercury.model.PlaylistId;
13-
import xyz.gianlu.librespot.mercury.model.TrackId;
14+
import xyz.gianlu.librespot.mercury.model.*;
1415

1516
import java.io.IOException;
1617

@@ -27,15 +28,40 @@ public MetadataHandler(@NotNull Session session) {
2728
this.client = session.mercury();
2829
}
2930

31+
@NotNull
32+
private static <I extends SpotifyId> I extractId(@NotNull Class<I> clazz, @NotNull ApiServer.Request request, @Nullable JsonElement params) throws ApiServer.PredefinedJsonRpcException {
33+
if (params == null || !params.isJsonObject())
34+
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.INVALID_PARAMS);
35+
36+
try {
37+
JsonObject obj = params.getAsJsonObject();
38+
if (obj.has("gid")) {
39+
return SpotifyId.fromHex(clazz, obj.get("gid").getAsString());
40+
} else if (obj.has("uri")) {
41+
return SpotifyId.fromUri(clazz, obj.get("uri").getAsString());
42+
} else if (obj.has("base62")) {
43+
return SpotifyId.fromBase62(clazz, obj.get("gid").getAsString());
44+
} else {
45+
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.INVALID_REQUEST);
46+
}
47+
} catch (SpotifyId.SpotifyIdParsingException ex) {
48+
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.INVALID_REQUEST);
49+
}
50+
}
51+
3052
@Override
3153
protected @NotNull JsonElement handleRequest(ApiServer.@NotNull Request request) throws ApiServer.PredefinedJsonRpcException, HandlingException {
3254
switch (request.getSuffix()) {
3355
case "rootlists":
3456
return handle(MercuryRequests.getRootPlaylists(session.apWelcome().getCanonicalUsername()));
3557
case "playlist":
36-
return handle(MercuryRequests.getPlaylist(PlaylistId.fromUri(request.params.getAsString())));
58+
return handle(MercuryRequests.getPlaylist(extractId(PlaylistId.class, request, request.params)));
3759
case "track":
38-
return handle(MercuryRequests.getTrack(TrackId.fromUri(request.params.getAsString())));
60+
return handle(MercuryRequests.getTrack(extractId(TrackId.class, request, request.params)));
61+
case "artist":
62+
return handle(MercuryRequests.getArtist(extractId(ArtistId.class, request, request.params)));
63+
case "album":
64+
return handle(MercuryRequests.getAlbum(extractId(AlbumId.class, request, request.params)));
3965
default:
4066
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.METHOD_NOT_FOUND);
4167
}
@@ -45,13 +71,14 @@ public MetadataHandler(@NotNull Session session) {
4571
private <P extends AbstractMessageLite> JsonElement handle(@NotNull ProtoJsonMercuryRequest<P> req) throws HandlingException {
4672
try {
4773
return client.sendSync(req).json();
48-
} catch (IOException | MercuryClient.MercuryException ex) {
49-
throw new HandlingException(ex, 100); // FIXME: Create error codes table
74+
} catch (MercuryClient.MercuryException ex) {
75+
throw new HandlingException(ex, ErrorCode.MERCURY_EXCEPTION);
76+
} catch (IOException ex) {
77+
throw new HandlingException(ex, ErrorCode.IO_EXCEPTION);
5078
}
5179
}
5280

5381
@Override
5482
protected void handleNotification(ApiServer.@NotNull Request request) {
55-
5683
}
5784
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,42 @@
44
import org.jetbrains.annotations.NotNull;
55
import xyz.gianlu.librespot.api.server.AbsApiHandler;
66
import xyz.gianlu.librespot.api.server.ApiServer;
7+
import xyz.gianlu.librespot.core.Session;
8+
import xyz.gianlu.librespot.player.Player;
79

810
/**
911
* @author Gianlu
1012
*/
1113
public class PlayerHandler extends AbsApiHandler {
12-
public PlayerHandler() {
14+
private final Player player;
15+
16+
public PlayerHandler(@NotNull Session session) {
1317
super("player");
18+
this.player = session.player();
1419
}
1520

1621
@Override
17-
protected @NotNull JsonElement handleRequest(ApiServer.@NotNull Request request) {
22+
protected @NotNull JsonElement handleRequest(ApiServer.@NotNull Request request) throws ApiServer.PredefinedJsonRpcException {
23+
switch (request.getSuffix()) {
24+
case "play":
25+
player.play();
26+
break;
27+
case "pause":
28+
player.pause();
29+
break;
30+
case "next":
31+
player.next();
32+
break;
33+
case "prev":
34+
player.previous();
35+
break;
36+
case "playPause":
37+
player.playPause();
38+
break;
39+
default:
40+
throw ApiServer.PredefinedJsonRpcException.from(request, ApiServer.PredefinedJsonRpcError.METHOD_NOT_FOUND);
41+
}
42+
1843
return string("OK");
1944
}
2045

api/src/main/java/xyz/gianlu/librespot/api/server/AbsApiHandler.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final void handle(@NotNull ApiServer.Request request) {
3535
JsonElement result = handleRequest(request);
3636
request.answerResult(result);
3737
} catch (HandlingException ex) {
38-
request.answerError(ex.code, ex.msg, ex.data);
38+
request.answerError(ex.code.code, ex.msg, ex.data);
3939
} catch (ApiServer.PredefinedJsonRpcException ex) {
4040
request.answerError(ex);
4141
}
@@ -46,30 +46,41 @@ final void handle(@NotNull ApiServer.Request request) {
4646

4747
protected abstract void handleNotification(@NotNull ApiServer.Request request);
4848

49+
public enum ErrorCode {
50+
MERCURY_EXCEPTION(1001),
51+
IO_EXCEPTION(1002);
52+
53+
public final int code;
54+
55+
ErrorCode(int code) {
56+
this.code = code;
57+
}
58+
}
59+
4960
protected static class HandlingException extends Exception {
50-
private final int code;
61+
private final ErrorCode code;
5162
private final String msg;
5263
private final JsonElement data;
5364

54-
public HandlingException(int code, @NotNull String msg, @Nullable JsonElement data) {
65+
public HandlingException(@NotNull ErrorCode code, @NotNull String msg, @Nullable JsonElement data) {
5566
super(msg);
5667
this.code = code;
5768
this.msg = msg;
5869
this.data = data;
5970
}
6071

61-
public HandlingException(int code, @NotNull String msg) {
72+
public HandlingException(@NotNull ErrorCode code, @NotNull String msg) {
6273
this(code, msg, null);
6374
}
6475

65-
public HandlingException(@NotNull Throwable cause, int code, @Nullable JsonElement data) {
76+
public HandlingException(@NotNull Throwable cause, @NotNull ErrorCode code, @Nullable JsonElement data) {
6677
super(cause);
6778
this.code = code;
6879
this.msg = cause.getMessage();
6980
this.data = data;
7081
}
7182

72-
public HandlingException(@NotNull Throwable cause, int code) {
83+
public HandlingException(@NotNull Throwable cause, @NotNull ErrorCode code) {
7384
this(cause, code, null);
7485
}
7586
}

core/src/main/java/xyz/gianlu/librespot/mercury/MercuryRequests.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import xyz.gianlu.librespot.common.proto.Metadata;
1212
import xyz.gianlu.librespot.common.proto.Playlist4Changes;
1313
import xyz.gianlu.librespot.common.proto.Playlist4Content;
14+
import xyz.gianlu.librespot.mercury.model.AlbumId;
15+
import xyz.gianlu.librespot.mercury.model.ArtistId;
1416
import xyz.gianlu.librespot.mercury.model.PlaylistId;
1517
import xyz.gianlu.librespot.mercury.model.TrackId;
1618

@@ -108,7 +110,7 @@ public final class MercuryRequests {
108110
@Override
109111
public @NotNull JsonElement convert(Metadata.@NotNull Artist proto) {
110112
JsonObject obj = new JsonObject();
111-
obj.addProperty("gid", Utils.toBase64(proto.getGid()));
113+
obj.addProperty("gid", Utils.bytesToHex(proto.getGid()));
112114
obj.addProperty("name", proto.getName());
113115
obj.addProperty("popularity", proto.getPopularity());
114116
obj.addProperty("isPortraitAlbumCover", proto.getIsPortraitAlbumCover());
@@ -132,7 +134,7 @@ public final class MercuryRequests {
132134
@Override
133135
public @NotNull JsonElement convert(Metadata.@NotNull Album proto) {
134136
JsonObject obj = new JsonObject();
135-
obj.addProperty("gid", Utils.toBase64(proto.getGid()));
137+
obj.addProperty("gid", Utils.bytesToHex(proto.getGid()));
136138
obj.addProperty("name", proto.getName());
137139
obj.addProperty("popularity", proto.getPopularity());
138140
obj.addProperty("label", proto.getLabel());
@@ -161,7 +163,7 @@ public final class MercuryRequests {
161163
@Override
162164
public @NotNull JsonElement convert(Metadata.@NotNull Track proto) {
163165
JsonObject obj = new JsonObject();
164-
obj.addProperty("gid", Utils.toBase64(proto.getGid()));
166+
obj.addProperty("gid", Utils.bytesToHex(proto.getGid()));
165167
obj.addProperty("name", proto.getName());
166168
obj.addProperty("number", proto.getNumber());
167169
obj.addProperty("discNumber", proto.getDiscNumber());
@@ -236,6 +238,16 @@ public static ProtoJsonMercuryRequest<Metadata.Track> getTrack(@NotNull TrackId
236238
return new ProtoJsonMercuryRequest<>(RawMercuryRequest.get(id.toMercuryUri()), Metadata.Track.parser(), TRACK_JSON_CONVERTER);
237239
}
238240

241+
@NotNull
242+
public static ProtoJsonMercuryRequest<Metadata.Artist> getArtist(@NotNull ArtistId id) {
243+
return new ProtoJsonMercuryRequest<>(RawMercuryRequest.get(id.toMercuryUri()), Metadata.Artist.parser(), ARTIST_JSON_CONVERTER);
244+
}
245+
246+
@NotNull
247+
public static ProtoJsonMercuryRequest<Metadata.Album> getAlbum(@NotNull AlbumId id) {
248+
return new ProtoJsonMercuryRequest<>(RawMercuryRequest.get(id.toMercuryUri()), Metadata.Album.parser(), ALBUM_JSON_CONVERTER);
249+
}
250+
239251
@NotNull
240252
public static ProtobufMercuryRequest<Mercury.MercuryMultiGetReply> multiGet(@NotNull String uri, Mercury.MercuryRequest... subs) {
241253
RawMercuryRequest.Builder request = RawMercuryRequest.newBuilder()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package xyz.gianlu.librespot.mercury.model;
2+
3+
import io.seruco.encoding.base62.Base62;
4+
import org.jetbrains.annotations.NotNull;
5+
import xyz.gianlu.librespot.common.Utils;
6+
7+
import java.math.BigInteger;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
10+
11+
/**
12+
* @author Gianlu
13+
*/
14+
public final class AlbumId implements SpotifyId {
15+
private static final Pattern PATTERN = Pattern.compile("spotify:album:(.{22})");
16+
private static final Base62 BASE62 = Base62.createInstanceWithInvertedCharacterSet();
17+
private final String hexId;
18+
19+
private AlbumId(@NotNull String hex) {
20+
this.hexId = hex;
21+
}
22+
23+
@NotNull
24+
public static AlbumId fromUri(@NotNull String uri) {
25+
Matcher matcher = PATTERN.matcher(uri);
26+
if (matcher.find()) {
27+
String id = matcher.group(1);
28+
return new AlbumId(Utils.bytesToHex(BASE62.decode(id.getBytes())));
29+
} else {
30+
throw new IllegalArgumentException("Not a Spotify album ID: " + uri);
31+
}
32+
}
33+
34+
@NotNull
35+
public static AlbumId fromBase62(@NotNull String base62) {
36+
return new AlbumId(Utils.bytesToHex(BASE62.decode(base62.getBytes())));
37+
}
38+
39+
@NotNull
40+
public static AlbumId fromHex(@NotNull String hex) {
41+
return new AlbumId(hex);
42+
}
43+
44+
@Override
45+
public @NotNull String toMercuryUri() {
46+
return "hm://metadata/4/album/" + hexId;
47+
}
48+
49+
@Override
50+
public @NotNull String toSpotifyUri() {
51+
return "spotify:album:" + new String(BASE62.encode(new BigInteger(hexId, 16).toByteArray()));
52+
}
53+
}

0 commit comments

Comments
 (0)