Skip to content

Commit 17ba408

Browse files
authored
Separating library from player (#245)
* Create `lib` module * [ci skip] Moved stuff to `lib` module + minor refactoring * [ci skip] Removed `common` module + renamed `core` to `player` * Refactored `api` module + renamed PlayerConfiguration.java + written startup scripts * Fixed `player.retryOnChunkError` configuration * Minor clean up + moved ZeroconfServer.java and FileConfiguration.java * Fixed stupid bugs * Fixed race condition when loading first track * Fixed startup scripts to actually create player * Modified READMEs
1 parent 0f080de commit 17ba408

171 files changed

Lines changed: 1983 additions & 1424 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,18 @@ Its main features are:
1919
- Gapless playback
2020
- Mixed playlists (cuepoints and transitions)
2121

22-
## Get started
23-
All the configuration you need is inside the `config.toml` file. If none is present, a sample `config.toml` will be generated the first time the jar is run. There you can decide to authenticate with:
24-
- Username and password
25-
- Zeroconf
26-
- Facebook
27-
- Auth blob
22+
## The library
23+
The `lib` module provides all the necessary components and tools to interact with Spotify. More [here](lib).
2824

29-
### Username and password
30-
This is pretty straightforward, but remember that having hardcoded passwords isn't the best thing on earth.
31-
32-
### Zeroconf
33-
In this mode `librespot` becomes discoverable with Spotify Connect by devices on the same network. Just open a Spotify client and select `librespot-java` from the available devices list.
34-
35-
If you have a firewall, you need to open the UDP port `5355` for mDNS. Then specify some random port in `zeroconf.listenPort` and open that TCP port too.
36-
37-
### Facebook
38-
Authenticate with Facebook. The console will provide a link to visit in order to continue the login process.
39-
40-
### Auth blob
41-
This is more advanced and should only be used if you saved an authentication blob. The blob should have already been Base64-decoded. Generating one is currently not a feature of librespot-java
42-
43-
### Storing credentials
44-
If the configurations `storeCredentials=true` and `credentialsFile="somepath.json"` have been set, the credentials will be saved in a more secure format inside the json file. After having run the application once and successfully authenticating, the authentication config fields above are no longer needed and should be made blank for security purposes.
45-
46-
## Run
47-
You can download the latest release from [here](https://github.com/librespot-org/librespot-java/releases) and then run `java -jar ./librespot-core-jar-with-dependencies.jar` from the command line.
48-
49-
### Audio output configuration
50-
On some systems, many mixers could be installed making librespot-java playback on the wrong one, therefore you won't hear anything and likely see an exception in the logs. If that's the case, follow the guide below:
51-
52-
1) In your configuration file (`config.toml` by default), under the `player` section, make sure `logAvailableMixers` is set to `true` and restart the application
53-
2) Connect to the client and start playing something
54-
3) Along with the previous exception there'll be a log message saying "Available mixers: ..."
55-
4) Pick the right mixer and copy its name inside the `mixerSearchKeywords` option. If you need to specify more search keywords, you can separate them with a semicolon
56-
5) Restart and enjoy
57-
58-
> **Linux note:** librespot-java will not be able to detect the mixers available on the system if you are running headless OpenJDK. You'll need to install a headful version of OpenJDK (usually doesn't end with `-headless`).
59-
60-
## Build it
61-
This project uses [Maven](https://maven.apache.org/), after installing it you can compile with `mvn clean package` in the project root, if the compilation succeeds you'll be pleased with a JAR executable in `core/target`.
62-
To run the newly build jar run `java -jar ./core/target/librespot-core-jar-with-dependencies.jar`.
25+
## The player
26+
The `player` module provides the full player experience. You can use it from Spotify Connect, and it operates in full headless mode. More [here](player).
6327

6428
## Protobuf generation
6529
The compiled Java protobuf definitions aren't versioned, therefore, if you want to open the project inside your IDE, you'll need to run `mvn compile` first to ensure that all the necessary files are created. If the build fails due to missing `protoc` you can install it manually and use the `-DprotocExecutable=/path/to/protoc` flag.
66-
6730
The `com.spotify` package is reserved for the generated files.
6831

6932
## Logging
70-
The application uses Log4J for logging purposes, the configuration file is placed inside `core/src/main/resources` or `api/src/main/resources` depending on what you're working with. You can also toggle the log level with `logLevel` option in the configuration.
33+
The application uses Log4J for logging purposes, the configuration file is placed inside `lib/src/main/resources`, `player/src/main/resources` or `api/src/main/resources` depending on what you're working with. You can also toggle the log level with `logLevel` option in the configuration.
7134

7235
## Related Projects
7336
- [librespot](https://github.com/librespot-org/librespot)

api/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@
5050
<dependencies>
5151
<dependency>
5252
<groupId>xyz.gianlu.librespot</groupId>
53-
<artifactId>librespot-core</artifactId>
53+
<artifactId>librespot-player</artifactId>
5454
<version>${project.version}</version>
5555
</dependency>
5656

5757
<dependency>
5858
<groupId>io.undertow</groupId>
5959
<artifactId>undertow-core</artifactId>
60-
<version>2.1.0.Final</version>
60+
<version>2.1.3.Final</version>
6161
</dependency>
6262
</dependencies>
6363
</project>

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

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

33
import io.undertow.Undertow;
4-
import io.undertow.server.HttpHandler;
54
import io.undertow.server.RoutingHandler;
65
import org.apache.logging.log4j.LogManager;
76
import org.apache.logging.log4j.Logger;
@@ -10,32 +9,30 @@
109

1110
public class ApiServer {
1211
private static final Logger LOGGER = LogManager.getLogger(ApiServer.class);
12+
protected final RoutingHandler handler;
13+
protected final EventsHandler events = new EventsHandler();
1314
private final int port;
1415
private final String host;
15-
private final HttpHandler handler;
1616
private Undertow undertow = null;
1717

18-
public ApiServer(@NotNull ApiConfiguration conf, @NotNull SessionWrapper wrapper) {
19-
this.port = conf.apiPort();
20-
this.host = conf.apiHost();
21-
22-
EventsHandler events = new EventsHandler();
23-
wrapper.setListener(events);
24-
25-
handler = new CorsHandler(new RoutingHandler()
26-
.post("/player/{cmd}", new PlayerHandler(wrapper))
18+
public ApiServer(int port, @NotNull String host, @NotNull SessionWrapper wrapper) {
19+
this.port = port;
20+
this.host = host;
21+
this.handler = new RoutingHandler()
2722
.post("/metadata/{type}/{uri}", new MetadataHandler(wrapper, true))
2823
.post("/metadata/{uri}", new MetadataHandler(wrapper, false))
2924
.post("/search/{query}", new SearchHandler(wrapper))
3025
.post("/token/{scope}", new TokensHandler(wrapper))
3126
.post("/profile/{user_id}/{action}", new ProfileHandler(wrapper))
32-
.get("/events", events));
27+
.get("/events", events);
28+
29+
wrapper.setListener(events);
3330
}
3431

3532
public void start() {
3633
if (undertow != null) throw new IllegalStateException("Already started!");
3734

38-
undertow = Undertow.builder().addHttpListener(port, host, handler).build();
35+
undertow = Undertow.builder().addHttpListener(port, host, new CorsHandler(handler)).build();
3936
undertow.start();
4037
LOGGER.info("Server started on port {}!", port);
4138
}

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33

44
import org.apache.logging.log4j.core.config.Configurator;
5-
import xyz.gianlu.librespot.AbsConfiguration;
6-
import xyz.gianlu.librespot.FileConfiguration;
5+
import org.jetbrains.annotations.NotNull;
76
import xyz.gianlu.librespot.common.Log4JUncaughtExceptionHandler;
8-
import xyz.gianlu.librespot.core.AuthConfiguration;
97
import xyz.gianlu.librespot.core.Session;
10-
import xyz.gianlu.librespot.core.ZeroconfServer;
118
import xyz.gianlu.librespot.mercury.MercuryClient;
9+
import xyz.gianlu.librespot.player.FileConfiguration;
10+
import xyz.gianlu.librespot.player.FileConfiguration.AuthStrategy;
1211

1312
import java.io.IOException;
1413
import java.security.GeneralSecurityException;
@@ -19,17 +18,37 @@
1918
public class Main {
2019

2120
public static void main(String[] args) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException {
22-
AbsConfiguration conf = new FileConfiguration(args);
21+
FileConfiguration conf = new FileConfiguration(args);
2322
Configurator.setRootLevel(conf.loggingLevel());
2423
Thread.setDefaultUncaughtExceptionHandler(new Log4JUncaughtExceptionHandler());
2524

25+
String host = conf.apiHost();
26+
int port = conf.apiPort();
27+
28+
if (args.length > 0 && args[0].equals("noPlayer")) withoutPlayer(port, host, conf);
29+
else withPlayer(port, host, conf);
30+
}
31+
32+
private static void withPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException {
33+
PlayerWrapper wrapper;
34+
if (conf.authStrategy() == AuthStrategy.ZEROCONF)
35+
wrapper = PlayerWrapper.fromZeroconf(conf.initZeroconfBuilder().create(), conf.toPlayer());
36+
else
37+
wrapper = PlayerWrapper.fromSession(conf.initSessionBuilder().create(), conf.toPlayer());
38+
39+
PlayerApiServer server = new PlayerApiServer(port, host, wrapper);
40+
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
41+
server.start();
42+
}
43+
44+
private static void withoutPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException {
2645
SessionWrapper wrapper;
27-
if (conf.authStrategy() == AuthConfiguration.Strategy.ZEROCONF && !conf.hasStoredCredentials())
28-
wrapper = SessionWrapper.fromZeroconf(ZeroconfServer.create(conf));
46+
if (conf.authStrategy() == AuthStrategy.ZEROCONF)
47+
wrapper = SessionWrapper.fromZeroconf(conf.initZeroconfBuilder().create());
2948
else
30-
wrapper = SessionWrapper.fromSession(new Session.Builder(conf).create());
49+
wrapper = SessionWrapper.fromSession(conf.initSessionBuilder().create());
3150

32-
ApiServer server = new ApiServer(conf, wrapper);
51+
ApiServer server = new ApiServer(port, host, wrapper);
3352
Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
3453
server.start();
3554
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package xyz.gianlu.librespot.api;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
import xyz.gianlu.librespot.api.handlers.PlayerHandler;
5+
6+
/**
7+
* @author devgianlu
8+
*/
9+
public class PlayerApiServer extends ApiServer {
10+
public PlayerApiServer(int port, @NotNull String host, @NotNull PlayerWrapper wrapper) {
11+
super(port, host, wrapper);
12+
13+
handler.post("/player/{cmd}", new PlayerHandler(wrapper));
14+
wrapper.setListener(events);
15+
}
16+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package xyz.gianlu.librespot.api;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
5+
import xyz.gianlu.librespot.ZeroconfServer;
6+
import xyz.gianlu.librespot.core.Session;
7+
import xyz.gianlu.librespot.player.Player;
8+
import xyz.gianlu.librespot.player.PlayerConfiguration;
9+
10+
import java.util.concurrent.atomic.AtomicReference;
11+
12+
/**
13+
* @author devgianlu
14+
*/
15+
public class PlayerWrapper extends SessionWrapper {
16+
private final AtomicReference<Player> playerRef = new AtomicReference<>(null);
17+
private final PlayerConfiguration conf;
18+
private Listener listener = null;
19+
20+
private PlayerWrapper(@NotNull PlayerConfiguration conf) {
21+
this.conf = conf;
22+
}
23+
24+
/**
25+
* Convenience method to create an instance of {@link PlayerWrapper} that is updated by {@link ZeroconfServer}
26+
*
27+
* @param server The {@link ZeroconfServer}
28+
* @param conf The player configuration
29+
* @return A wrapper that holds a changing session-player tuple
30+
*/
31+
@NotNull
32+
public static PlayerWrapper fromZeroconf(@NotNull ZeroconfServer server, @NotNull PlayerConfiguration conf) {
33+
PlayerWrapper wrapper = new PlayerWrapper(conf);
34+
server.addSessionListener(wrapper::set);
35+
return wrapper;
36+
}
37+
38+
/**
39+
* Convenience method to create an instance of {@link PlayerWrapper} that holds a static session and player
40+
*
41+
* @param session The static session
42+
* @param conf The player configuration
43+
* @return A wrapper that holds a never-changing session-player tuple
44+
*/
45+
@NotNull
46+
public static PlayerWrapper fromSession(@NotNull Session session, @NotNull PlayerConfiguration conf) {
47+
PlayerWrapper wrapper = new PlayerWrapper(conf);
48+
wrapper.sessionRef.set(session);
49+
wrapper.playerRef.set(new Player(conf, session));
50+
return wrapper;
51+
}
52+
53+
public void setListener(@NotNull Listener listener) {
54+
super.setListener(listener);
55+
this.listener = listener;
56+
57+
Player p;
58+
if ((p = playerRef.get()) != null) listener.onNewPlayer(p);
59+
}
60+
61+
@Override
62+
protected void set(@NotNull Session session) {
63+
super.set(session);
64+
65+
Player player = new Player(conf, session);
66+
playerRef.set(player);
67+
68+
if (listener != null) listener.onNewPlayer(player);
69+
}
70+
71+
@Override
72+
protected void clear() {
73+
super.clear();
74+
75+
Player old = playerRef.get();
76+
if (old != null) old.close();
77+
playerRef.set(null);
78+
79+
if (listener != null && old != null) listener.onPlayerCleared(old);
80+
}
81+
82+
@Nullable
83+
public Player getPlayer() {
84+
return playerRef.get();
85+
}
86+
87+
public interface Listener extends SessionWrapper.Listener {
88+
void onPlayerCleared(@NotNull Player old);
89+
90+
void onNewPlayer(@NotNull Player player);
91+
}
92+
}

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22

33
import org.jetbrains.annotations.NotNull;
44
import org.jetbrains.annotations.Nullable;
5+
import xyz.gianlu.librespot.ZeroconfServer;
56
import xyz.gianlu.librespot.core.Session;
6-
import xyz.gianlu.librespot.core.ZeroconfServer;
77

88
import java.util.concurrent.atomic.AtomicReference;
99

1010
/**
1111
* @author Gianlu
1212
*/
13-
public final class SessionWrapper {
14-
private final AtomicReference<Session> ref = new AtomicReference<>(null);
13+
public class SessionWrapper {
14+
protected final AtomicReference<Session> sessionRef = new AtomicReference<>(null);
1515
private Listener listener = null;
1616

17-
private SessionWrapper() {
17+
protected SessionWrapper() {
1818
}
1919

2020
/**
@@ -39,32 +39,32 @@ public static SessionWrapper fromZeroconf(@NotNull ZeroconfServer server) {
3939
@NotNull
4040
public static SessionWrapper fromSession(@NotNull Session session) {
4141
SessionWrapper wrapper = new SessionWrapper();
42-
wrapper.ref.set(session);
42+
wrapper.sessionRef.set(session);
4343
return wrapper;
4444
}
4545

4646
public void setListener(@NotNull Listener listener) {
4747
this.listener = listener;
4848

4949
Session s;
50-
if ((s = ref.get()) != null) listener.onNewSession(s);
50+
if ((s = sessionRef.get()) != null) listener.onNewSession(s);
5151
}
5252

53-
private void set(@NotNull Session session) {
54-
ref.set(session);
53+
protected void set(@NotNull Session session) {
54+
sessionRef.set(session);
5555
session.addCloseListener(this::clear);
5656
if (listener != null) listener.onNewSession(session);
5757
}
5858

59-
private void clear() {
60-
Session old = ref.get();
61-
ref.set(null);
59+
protected void clear() {
60+
Session old = sessionRef.get();
61+
sessionRef.set(null);
6262
if (listener != null && old != null) listener.onSessionCleared(old);
6363
}
6464

6565
@Nullable
66-
public Session get() {
67-
Session s = ref.get();
66+
public Session getSession() {
67+
Session s = sessionRef.get();
6868
if (s != null) {
6969
if (s.isValid()) return s;
7070
else clear();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package xyz.gianlu.librespot.api.handlers;
2+
3+
import io.undertow.server.HttpServerExchange;
4+
import io.undertow.util.StatusCodes;
5+
import org.jetbrains.annotations.NotNull;
6+
import xyz.gianlu.librespot.api.PlayerWrapper;
7+
import xyz.gianlu.librespot.core.Session;
8+
import xyz.gianlu.librespot.player.Player;
9+
10+
/**
11+
* @author devgianlu
12+
*/
13+
public abstract class AbsPlayerHandler extends AbsSessionHandler {
14+
private final PlayerWrapper wrapper;
15+
16+
public AbsPlayerHandler(@NotNull PlayerWrapper wrapper) {
17+
super(wrapper);
18+
this.wrapper = wrapper;
19+
}
20+
21+
@Override
22+
protected final void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session) throws Exception {
23+
Player player = wrapper.getPlayer();
24+
if (player == null) {
25+
exchange.setStatusCode(StatusCodes.NO_CONTENT);
26+
return;
27+
}
28+
29+
handleRequest(exchange, session, player);
30+
}
31+
32+
protected abstract void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session session, @NotNull Player player) throws Exception;
33+
}

0 commit comments

Comments
 (0)