Skip to content

Commit 22aed9c

Browse files
committed
Merge remote-tracking branch 'origin/dev' into dev
2 parents 87a35f3 + 4e58628 commit 22aed9c

25 files changed

Lines changed: 486 additions & 112 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ This project uses [Maven](https://maven.apache.org/), after installing it you ca
5555
To run the newly build jar run `java -jar ./core/target/librespot-core-jar-with-dependencies.jar`.
5656

5757
## Related Projects
58-
[ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot-Java.
58+
- [librespot](https://github.com/librespot-org/librespot)
59+
- [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot-Java.
5960

6061
# Special thanks
6162

6263
- All the developers of [librespot](https://github.com/librespot-org/librespot) which started this project in Rust
63-
- All the contributors of this project for testing and fixing stuff, especially [@loeffelpan](https://github.com/loeffelpan), [@crsmoro](https://github.com/crsmoro) and [@Schluggi](https://github.com/Schluggi) for the feedback provided
64+
- All the contributors of this project for testing and fixing stuff
6465
- <a href="https://www.yourkit.com/"><img src="https://www.yourkit.com/images/yklogo.png" height="20"></a> that provided a free license for their [Java Profiler](https://www.yourkit.com/java/profiler/)

api/README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@
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+
- `POST \player\current` Retrieve information about the current track.
18+
19+
### Metadata
20+
- `POST \metadata\{type}\{uri}` Retrieve metadata. `type` can be one of `episode`, `track`, `album`, `show`, `artist`, `uri` is the standard Spotify uri.
21+
22+
23+
## Examples
24+
25+
`curl -X POST -d "uri=spotify:track:xxxxxxxxxxxxxxxxxxxxxx&play=true" http://localhost:24879/player/load`
26+
27+
`curl -X POST http://localhost:24879/metadata/track/spotify:track:xxxxxxxxxxxxxxxxxxxxxx`

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>0.6.2</version>
8+
<version>1.0.0</version>
99
<relativePath>../</relativePath>
1010
</parent>
1111

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: 10 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,13 @@ 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+
internalError(exchange, ex.getMessage());
64+
}
65+
66+
public static void internalError(@NotNull HttpServerExchange exchange, @NotNull String reason) {
67+
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
68+
exchange.getResponseSender().send(String.format(INTERNAL_ERROR_BODY, reason));
69+
}
6070
}
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+
}

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package xyz.gianlu.librespot.api.handlers;
22

3+
import com.google.gson.JsonObject;
4+
import com.spotify.metadata.proto.Metadata;
35
import io.undertow.server.HttpHandler;
46
import io.undertow.server.HttpServerExchange;
57
import org.jetbrains.annotations.NotNull;
68
import org.jetbrains.annotations.Nullable;
79
import xyz.gianlu.librespot.api.Utils;
10+
import xyz.gianlu.librespot.common.ProtobufToJson;
811
import xyz.gianlu.librespot.core.Session;
12+
import xyz.gianlu.librespot.mercury.model.EpisodeId;
13+
import xyz.gianlu.librespot.mercury.model.PlayableId;
14+
import xyz.gianlu.librespot.mercury.model.TrackId;
915
import xyz.gianlu.librespot.player.PlayerRunner;
1016

1117
import java.util.Deque;
@@ -50,6 +56,36 @@ private void load(HttpServerExchange exchange, @Nullable String uri, boolean pla
5056
session.player().load(uri, play);
5157
}
5258

59+
private void current(HttpServerExchange exchange) {
60+
PlayableId id = session.player().currentPlayableId();
61+
62+
JsonObject obj;
63+
if (id instanceof TrackId) {
64+
Metadata.Track track = session.player().currentTrack();
65+
if (track == null) {
66+
Utils.internalError(exchange, "Missing track metadata. Try again.");
67+
return;
68+
}
69+
70+
obj = ProtobufToJson.convert(track);
71+
obj.addProperty("uri", id.toSpotifyUri());
72+
} else if (id instanceof EpisodeId) {
73+
Metadata.Episode episode = session.player().currentEpisode();
74+
if (episode == null) {
75+
Utils.internalError(exchange, "Missing episode metadata. Try again.");
76+
return;
77+
}
78+
79+
obj = ProtobufToJson.convert(episode);
80+
obj.addProperty("uri", id.toSpotifyUri());
81+
} else {
82+
Utils.internalError(exchange, "Invalid PlayableId: " + id);
83+
return;
84+
}
85+
86+
exchange.getResponseSender().send(obj.toString());
87+
}
88+
5389
@Override
5490
public void handleRequest(HttpServerExchange exchange) throws Exception {
5591
exchange.startBlocking();
@@ -72,6 +108,9 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
72108
}
73109

74110
switch (cmd) {
111+
case CURRENT:
112+
current(exchange);
113+
return;
75114
case SET_VOLUME:
76115
setVolume(exchange, Utils.getFirstString(params, "volume"));
77116
return;
@@ -104,7 +143,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception {
104143
private enum Command {
105144
LOAD("load"), PAUSE("pause"), RESUME("resume"),
106145
NEXT("next"), PREV("prev"), SET_VOLUME("set-volume"),
107-
VOLUME_UP("volume-up"), VOLUME_DOWN("volume-down");
146+
VOLUME_UP("volume-up"), VOLUME_DOWN("volume-down"), CURRENT("current");
108147

109148
private String name;
110149

common/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>0.6.2</version>
8+
<version>1.0.0</version>
99
<relativePath>../</relativePath>
1010
</parent>
1111

core/pom.xml

Lines changed: 5 additions & 5 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>0.6.2</version>
8+
<version>1.0.0</version>
99
<relativePath>../</relativePath>
1010
</parent>
1111

@@ -63,11 +63,11 @@
6363
<version>1.0.2-gdx</version>
6464
</dependency>
6565

66-
<!-- mDNS -->
66+
<!-- Zeroconf -->
6767
<dependency>
68-
<groupId>org.jmdns</groupId>
69-
<artifactId>jmdns</artifactId>
70-
<version>3.5.5</version>
68+
<groupId>xyz.gianlu.zeroconf</groupId>
69+
<artifactId>zeroconf</artifactId>
70+
<version>1.1.2</version>
7171
</dependency>
7272

7373
<!-- H2 (cache) -->

core/src/main/java/xyz/gianlu/librespot/FileConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ public int crossfadeDuration() {
285285
return config.get("player.crossfadeDuration");
286286
}
287287

288+
@Override
289+
public int releaseLineDelay() {
290+
return config.get("player.releaseLineDelay");
291+
}
292+
288293
@Override
289294
public @Nullable String deviceName() {
290295
return config.get("deviceName");

0 commit comments

Comments
 (0)