diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 8d1bb8d9b..a496c1432 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -104,6 +104,16 @@ Set player volume. Volume may range from 0 to 150. 100 is default. } ``` +Tell the server to potentially disconnect from the voice server and potentially remove the player with all its data. +This is useful if you want to move to a new node for a voice connection. Calling this op does not affect voice state, +and you can send the same VOICE_SERVER_UPDATE to a new node. +```json +{ + "op": "destroy", + "guildId": "..." +} +``` + ### Incoming messages See [LavalinkSocket.java](https://github.com/Frederikam/Lavalink/blob/dev/LavalinkClient/src/main/java/lavalink/client/io/LavalinkSocket.java) diff --git a/LavalinkClient/build.gradle b/LavalinkClient/build.gradle index 98de1996d..7ef43e9ae 100644 --- a/LavalinkClient/build.gradle +++ b/LavalinkClient/build.gradle @@ -1,19 +1,47 @@ description = 'JDA based client for the Lavalink-Server' -version System.getenv('dev') == 'true' ? '-SNAPSHOT' : '2.0' +version System.getenv('dev') == 'true' ? '-SNAPSHOT' : '2.0.1' ext { moduleName = 'Lavalink-Client' } + +apply plugin: 'maven-publish' + +publishing { + publications { + mavenJava(MavenPublication) { + groupId rootProject.group + artifactId moduleName + + from components.java + + artifact sourceJar { + classifier "sources" + } + } + } +} + +task install(dependsOn: 'publishToMavenLocal') +publishToMavenLocal.dependsOn 'jar' + +test { + useJUnitPlatform() + + systemProperty("TEST_TOKEN", System.getProperty("TEST_TOKEN")) + systemProperty("TEST_VOICE_CHANNEL", System.getProperty("TEST_VOICE_CHANNEL")) +} + dependencies { - compile group: 'com.sedmelluq', name: 'lavaplayer', version: '1.2.47' - compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.3.7' - compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' - compile group: 'org.json', name: 'json', version: '20180130' - compile group: 'net.dv8tion', name: 'JDA', version: '3.5.0_334' - compileOnly group: 'io.prometheus', name: 'simpleclient', version: '0.1.0' - testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.0.0-M4' - testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.0.0-M4' - testCompile group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.0.0-M4' - testCompile group: 'org.junit.platform', name: 'junit-platform-runner', version: '1.0.0-M4' - testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' - testCompile group: 'com.mashape.unirest', name: 'unirest-java', version: '1.4.9' + compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion + compile group: 'org.java-websocket', name: 'Java-WebSocket', version: javaWebSocketVersion + compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion + compile group: 'org.json', name: 'json', version: jsonOrgVersion + compile group: 'net.dv8tion', name: 'JDA', version: jdaVersion + compileOnly group: 'io.prometheus', name: 'simpleclient', version: prometheusVersion + testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitJupiterVersion + testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitJupiterVersion + testCompile group: 'org.junit.platform', name: 'junit-platform-launcher', version: junitPlatformVersion + testCompile group: 'org.junit.platform', name: 'junit-platform-runner', version: junitPlatformVersion + testCompile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion + testCompile group: 'com.mashape.unirest', name: 'unirest-java', version: unirestVersion } diff --git a/LavalinkClient/src/main/java/lavalink/client/io/Lavalink.java b/LavalinkClient/src/main/java/lavalink/client/io/Lavalink.java index 11a0bf34e..a8057c54b 100644 --- a/LavalinkClient/src/main/java/lavalink/client/io/Lavalink.java +++ b/LavalinkClient/src/main/java/lavalink/client/io/Lavalink.java @@ -108,7 +108,9 @@ public void addNode(@Nonnull String name, @Nonnull URI serverUri, @Nonnull Strin headers.put("Num-Shards", Integer.toString(numShards)); headers.put("User-Id", userId); - nodes.add(new LavalinkSocket(name, this, serverUri, new Draft_6455(), headers)); + LavalinkSocket socket = new LavalinkSocket(name, this, serverUri, new Draft_6455(), headers); + socket.connect(); + nodes.add(socket); } @SuppressWarnings("unused") diff --git a/LavalinkClient/src/main/java/lavalink/client/io/LavalinkSocket.java b/LavalinkClient/src/main/java/lavalink/client/io/LavalinkSocket.java index 03e17511e..3434e7852 100644 --- a/LavalinkClient/src/main/java/lavalink/client/io/LavalinkSocket.java +++ b/LavalinkClient/src/main/java/lavalink/client/io/LavalinkSocket.java @@ -66,11 +66,6 @@ public class LavalinkSocket extends ReusableWebSocket { this.name = name; this.lavalink = lavalink; this.remoteUri = serverUri; - try { - this.connectBlocking(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } } @Override diff --git a/LavalinkClient/src/main/java/lavalink/client/io/Link.java b/LavalinkClient/src/main/java/lavalink/client/io/Link.java index 27a1bb45d..7c248e710 100644 --- a/LavalinkClient/src/main/java/lavalink/client/io/Link.java +++ b/LavalinkClient/src/main/java/lavalink/client/io/Link.java @@ -106,15 +106,17 @@ void connect(VoiceChannel channel, boolean checkChannel) { if (checkChannel && channel.equals(channel.getGuild().getSelfMember().getVoiceState().getChannel())) return; - final int userLimit = channel.getUserLimit(); // userLimit is 0 if no limit is set! - if (!self.isOwner() && !self.hasPermission(Permission.ADMINISTRATOR)) { - final long perms = PermissionUtil.getExplicitPermission(channel, self); - final long voicePerm = Permission.VOICE_MOVE_OTHERS.getRawValue(); - if (userLimit > 0 // If there is a userlimit - && userLimit <= channel.getMembers().size() // if that userlimit is reached - && (perms & voicePerm) != voicePerm) // If we don't have voice move others permissions - throw new InsufficientPermissionException(Permission.VOICE_MOVE_OTHERS, // then throw exception! - "Unable to connect to VoiceChannel due to userlimit! Requires permission VOICE_MOVE_OTHERS to bypass"); + if (channel.getGuild().getSelfMember().getVoiceState().inVoiceChannel()) { + final int userLimit = channel.getUserLimit(); // userLimit is 0 if no limit is set! + if (!self.isOwner() && !self.hasPermission(Permission.ADMINISTRATOR)) { + final long perms = PermissionUtil.getExplicitPermission(channel, self); + final long voicePerm = Permission.VOICE_MOVE_OTHERS.getRawValue(); + if (userLimit > 0 // If there is a userlimit + && userLimit <= channel.getMembers().size() // if that userlimit is reached + && (perms & voicePerm) != voicePerm) // If we don't have voice move others permissions + throw new InsufficientPermissionException(Permission.VOICE_MOVE_OTHERS, // then throw exception! + "Unable to connect to VoiceChannel due to userlimit! Requires permission VOICE_MOVE_OTHERS to bypass"); + } } setState(State.CONNECTING); @@ -165,8 +167,9 @@ void onDisconnected() { */ @SuppressWarnings("unused") public void destroy() { + boolean shouldDisconnect = state != State.DISCONNECTING && state != State.NOT_CONNECTED; setState(State.DESTROYING); - if (state != State.DISCONNECTING && state != State.NOT_CONNECTED) { + if (shouldDisconnect) { Guild g = getJda().getGuildById(guild); if (g != null) getMainWs().queueAudioDisconnect(g); } @@ -199,7 +202,7 @@ public LavalinkSocket getNode() { public LavalinkSocket getNode(boolean selectIfAbsent) { if (selectIfAbsent && node == null) { node = lavalink.loadBalancer.determineBestSocket(guild); - player.onNodeChange(); + if (player != null) player.onNodeChange(); } return node; } diff --git a/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java b/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java index d0c544320..c8db99c69 100644 --- a/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java +++ b/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java @@ -24,6 +24,7 @@ import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import lavalink.client.LavalinkUtil; +import lavalink.client.io.LavalinkSocket; import lavalink.client.io.Link; import lavalink.client.player.event.IPlayerEventListener; import lavalink.client.player.event.PlayerEvent; @@ -92,7 +93,9 @@ public void playTrack(AudioTrack track) { json.put("endTime", trackData.endPos); } json.put("pause", paused); + //noinspection ConstantConditions link.getNode(true).send(json.toString()); + updateTime = System.currentTimeMillis(); this.track = track; emitEvent(new TrackStartEvent(this, track)); @@ -103,22 +106,27 @@ public void playTrack(AudioTrack track) { @Override public void stopTrack() { + track = null; + + LavalinkSocket node = link.getNode(false); + if (node == null) return; JSONObject json = new JSONObject(); json.put("op", "stop"); json.put("guildId", link.getGuildId()); - link.getNode(true).send(json.toString()); - track = null; + node.send(json.toString()); } @Override public void setPaused(boolean pause) { if (pause == paused) return; - - JSONObject json = new JSONObject(); - json.put("op", "pause"); - json.put("guildId", link.getGuildId()); - json.put("pause", pause); - link.getNode(true).send(json.toString()); + LavalinkSocket node = link.getNode(false); + if (node != null) { + JSONObject json = new JSONObject(); + json.put("op", "pause"); + json.put("guildId", link.getGuildId()); + json.put("pause", pause); + node.send(json.toString()); + } paused = pause; if (pause) { @@ -156,19 +164,23 @@ public void seekTo(long position) { json.put("op", "seek"); json.put("guildId", link.getGuildId()); json.put("position", position); + //noinspection ConstantConditions link.getNode(true).send(json.toString()); } @Override public void setVolume(int volume) { volume = Math.min(150, Math.max(0, volume)); // Lavaplayer bounds + this.volume = volume; + + LavalinkSocket node = link.getNode(false); + if (node == null) return; JSONObject json = new JSONObject(); json.put("op", "volume"); json.put("guildId", link.getGuildId()); json.put("volume", volume); - link.getNode(true).send(json.toString()); - this.volume = volume; + node.send(json.toString()); } @Override diff --git a/LavalinkClient/src/test/java/lavalink/client/LavalinkTest.java b/LavalinkClient/src/test/java/lavalink/client/LavalinkTest.java index 1c8c6203e..df5ceb3cb 100644 --- a/LavalinkClient/src/test/java/lavalink/client/LavalinkTest.java +++ b/LavalinkClient/src/test/java/lavalink/client/LavalinkTest.java @@ -35,8 +35,8 @@ import net.dv8tion.jda.core.JDABuilder; import net.dv8tion.jda.core.entities.VoiceChannel; import org.json.JSONArray; +import org.json.JSONObject; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -50,10 +50,22 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@RequireSystemProperty({ + LavalinkTest.PROPERTY_TOKEN, + LavalinkTest.PROPERTY_CHANNEL, +}) class LavalinkTest { private static final Logger log = LoggerFactory.getLogger(LavalinkTest.class); + public static final String PROPERTY_TOKEN = "TEST_TOKEN"; + public static final String PROPERTY_CHANNEL = "TEST_VOICE_CHANNEL"; + private static JDA jda = null; private static Lavalink lavalink = null; private static final String[] BILL_WURTZ_JINGLES = { @@ -67,37 +79,40 @@ class LavalinkTest { }; @BeforeAll - static void setUp() { - try { - jda = new JDABuilder(AccountType.BOT) - .setToken(System.getenv("TEST_TOKEN")) - .addEventListener(lavalink) - .buildBlocking(); - - lavalink = new Lavalink("152691313123393536", 1, integer -> jda); - lavalink.addNode(new URI("ws://localhost"), "youshallnotpass"); - } catch (Exception e) { - throw new RuntimeException(e); - } + static void setUp() throws Exception { + JDABuilder jdaBuilder = new JDABuilder(AccountType.BOT) + .setToken(getSystemProperty(PROPERTY_TOKEN)); + + JDA selfId = jdaBuilder.buildAsync(); + lavalink = new Lavalink(selfId.asBot().getApplicationInfo().submit().get(30, TimeUnit.SECONDS).getId(), 1, integer -> jda); + selfId.shutdown(); + + lavalink.addNode(new URI("ws://localhost:5555"), "youshallnotpass"); + + jda = jdaBuilder + .addEventListener(lavalink) + .buildAsync(); + + Thread.sleep(2000); + assertTrue(lavalink.getNodes().get(0).isAvailable(), "Could not connect to lavalink server"); } @AfterAll static void tearDown() { - lavalink.shutdown(); - jda.shutdown(); + if (lavalink != null) { + lavalink.shutdown(); + } + if (jda != null) { + jda.shutdown(); + } } @Test void vcJoinTest() { - VoiceChannel vc = jda.getVoiceChannelById(System.getenv("TEST_VOICE_CHANNEL")); - lavalink.openVoiceConnection(vc); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - lavalink.closeVoiceConnection(vc.getGuild()); + VoiceChannel vc = fetchVoiceChannel(jda, getTestVoiceChannelId()); + ensureConnected(lavalink, vc); + assertEquals(Link.State.CONNECTED, lavalink.getLink(vc.getGuild()).getState(), "Failed to connect to voice channel"); + ensureNotConnected(lavalink, vc); } private List loadAudioTracks(String identifier) { @@ -106,13 +121,12 @@ private List loadAudioTracks(String identifier) { .header("Authorization", "youshallnotpass") .asJson() .getBody() - .getObject() - .getJSONArray("tracks"); + .getArray(); ArrayList list = new ArrayList<>(); trackData.forEach(o -> { try { - list.add(LavalinkUtil.toAudioTrack((String) o)); + list.add(LavalinkUtil.toAudioTrack(((JSONObject) o).getString("track"))); } catch (IOException e) { throw new RuntimeException(e); } @@ -125,10 +139,10 @@ private List loadAudioTracks(String identifier) { } private void connectAndPlay(AudioTrack track) throws InterruptedException { - VoiceChannel vc = jda.getVoiceChannelById(System.getenv("TEST_VOICE_CHANNEL")); - lavalink.openVoiceConnection(vc); + VoiceChannel vc = fetchVoiceChannel(jda, getTestVoiceChannelId()); + ensureConnected(lavalink, vc); - IPlayer player = lavalink.getPlayer(vc.getGuild().getId()); + IPlayer player = lavalink.getLink(vc.getGuild()).getPlayer(); CountDownLatch latch = new CountDownLatch(1); PlayerEventListenerAdapter listener = new PlayerEventListenerAdapter() { @Override @@ -141,11 +155,11 @@ public void onTrackStart(IPlayer player, AudioTrack track) { player.playTrack(track); latch.await(5, TimeUnit.SECONDS); - lavalink.closeVoiceConnection(vc.getGuild()); + ensureNotConnected(lavalink, vc); player.removeListener(listener); player.stopTrack(); - Assertions.assertEquals(0, latch.getCount()); + assertEquals(0, latch.getCount()); } @Test @@ -160,10 +174,10 @@ void vcStreamTest() throws InterruptedException { @Test void stopTest() throws InterruptedException { - VoiceChannel vc = jda.getVoiceChannelById(System.getenv("TEST_VOICE_CHANNEL")); - lavalink.openVoiceConnection(vc); + VoiceChannel vc = fetchVoiceChannel(jda, getTestVoiceChannelId()); + ensureConnected(lavalink, vc); - IPlayer player = lavalink.getPlayer(vc.getGuild().getId()); + IPlayer player = lavalink.getLink(vc.getGuild()).getPlayer(); CountDownLatch latch = new CountDownLatch(1); PlayerEventListenerAdapter listener = new PlayerEventListenerAdapter() { @@ -185,18 +199,18 @@ public void onTrackEnd(IPlayer player, AudioTrack track, AudioTrackEndReason end player.playTrack(loadAudioTracks("aGOFOP2BIhI").get(0)); latch.await(5, TimeUnit.SECONDS); - lavalink.closeVoiceConnection(vc.getGuild()); + ensureNotConnected(lavalink, vc); player.removeListener(listener); player.stopTrack(); - Assertions.assertEquals(0, latch.getCount()); + assertEquals(0, latch.getCount()); } @Test void testPlayback() throws InterruptedException { - VoiceChannel vc = jda.getVoiceChannelById(System.getenv("TEST_VOICE_CHANNEL")); + VoiceChannel vc = fetchVoiceChannel(jda, getTestVoiceChannelId()); Link link = lavalink.getLink(vc.getGuild()); - link.connect(vc); + ensureConnected(lavalink, vc); IPlayer player = link.getPlayer(); CountDownLatch latch = new CountDownLatch(1); @@ -204,6 +218,7 @@ void testPlayback() throws InterruptedException { PlayerEventListenerAdapter listener = new PlayerEventListenerAdapter() { @Override public void onTrackEnd(IPlayer player, AudioTrack track, AudioTrackEndReason endReason) { + log.info(endReason.name()); if (endReason == AudioTrackEndReason.FINISHED) { latch.countDown(); } @@ -217,12 +232,82 @@ public void onTrackEnd(IPlayer player, AudioTrack track, AudioTrackEndReason end player.playTrack(loadAudioTracks(jingle).get(0)); latch.await(20, TimeUnit.SECONDS); - link.disconnect(); + ensureNotConnected(lavalink, vc); player.removeListener(listener); player.stopTrack(); - Assertions.assertEquals(0, latch.getCount()); + assertEquals(0, latch.getCount()); + } + + private static String getSystemProperty(String key) { + String value = System.getProperty(key); + + assertNotNull(value, "Missing system property " + key); + assertFalse(value.isEmpty(), "System property " + key + " is empty"); + + return value; } + private static long getTestVoiceChannelId() { + return Long.parseUnsignedLong(getSystemProperty(PROPERTY_CHANNEL)); + } + + private static VoiceChannel fetchVoiceChannel(JDA jda, long voiceChannelId) { + long started = System.currentTimeMillis(); + while (jda.getStatus() != JDA.Status.CONNECTED + && System.currentTimeMillis() - started < 10000 //wait 10 sec max + && !Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + assertEquals(JDA.Status.CONNECTED, jda.getStatus(), "Failed to connect to Discord in a reasonable amount of time"); + + VoiceChannel voiceChannel = jda.getVoiceChannelById(voiceChannelId); + assertNotNull(voiceChannel, "Configured VoiceChannel not found on the configured Discord bot account"); + + return voiceChannel; + } + + + private static void ensureConnected(Lavalink lavalink, VoiceChannel voiceChannel) { + + Link link = lavalink.getLink(voiceChannel.getGuild()); + link.connect(voiceChannel); + long started = System.currentTimeMillis(); + while (link.getState() != Link.State.CONNECTED + && System.currentTimeMillis() - started < 10000 //wait 10 sec max + && !Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + link.connect(voiceChannel); + } + + assertEquals(Link.State.CONNECTED, link.getState(), "Failed to connect to voice channel in a reasonable amount of time"); + } + + private static void ensureNotConnected(Lavalink lavalink, VoiceChannel voiceChannel) { + Link link = lavalink.getLink(voiceChannel.getGuild()); + link.disconnect(); + long started = System.currentTimeMillis(); + while (link.getState() != Link.State.NOT_CONNECTED && link.getState() != Link.State.DISCONNECTING + && System.currentTimeMillis() - started < 10000 //wait 10 sec max + && !Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + link.disconnect(); + } + + assertTrue(link.getState() == Link.State.NOT_CONNECTED + || link.getState() == Link.State.DISCONNECTING, "Failed to disconnect from voice channel in a reasonable amount of time"); + } } diff --git a/LavalinkClient/src/test/java/lavalink/client/RequireSystemProperty.java b/LavalinkClient/src/test/java/lavalink/client/RequireSystemProperty.java new file mode 100644 index 000000000..b3208c94c --- /dev/null +++ b/LavalinkClient/src/test/java/lavalink/client/RequireSystemProperty.java @@ -0,0 +1,19 @@ +package lavalink.client; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by napster on 06.03.18. + */ +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(RequireSystemPropertyExists.class) +public @interface RequireSystemProperty { + + /** + * @return an array of System property keys + */ + String[] value(); +} diff --git a/LavalinkClient/src/test/java/lavalink/client/RequireSystemPropertyExists.java b/LavalinkClient/src/test/java/lavalink/client/RequireSystemPropertyExists.java new file mode 100644 index 000000000..75aa44239 --- /dev/null +++ b/LavalinkClient/src/test/java/lavalink/client/RequireSystemPropertyExists.java @@ -0,0 +1,31 @@ +package lavalink.client; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.support.AnnotationSupport; + +import java.util.Optional; + +/** + * Created by napster on 06.03.18. + *

+ * Checks whether the required system properties have been set + */ +public class RequireSystemPropertyExists implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional annotation = AnnotationSupport.findAnnotation(context.getElement(), RequireSystemProperty.class); + if (annotation.isPresent()) { + for (String propertyKey : annotation.get().value()) { + String propertyValue = System.getProperty(propertyKey); + if (propertyValue == null || propertyValue.isEmpty()) { + return ConditionEvaluationResult.disabled(String.format("System property '%s' not set. Skipping test.", propertyKey)); + } + } + return ConditionEvaluationResult.enabled("All required system properties present. Continuing test."); + } + return ConditionEvaluationResult.enabled("No RequireSystemProperty annotation found. Continuing test."); + } +} diff --git a/LavalinkServer/.gitignore b/LavalinkServer/.gitignore index ffb8ea2ec..3d29ec6be 100644 --- a/LavalinkServer/.gitignore +++ b/LavalinkServer/.gitignore @@ -6,3 +6,4 @@ /debug_token build/* /out/ +VERSION.txt diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index e56811464..7563f847d 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -1,32 +1,50 @@ -plugins { - id 'application' - id 'org.springframework.boot' version '1.5.10.RELEASE' - id 'com.gorylenko.gradle-git-properties' version '1.4.20' -} +apply plugin: 'application' +apply plugin: 'org.springframework.boot' +apply plugin: 'com.gorylenko.gradle-git-properties' description = 'Play audio to discord voice channels' mainClassName = "lavalink.server.Launcher" -version '2.0' +version '2.0.1' ext { moduleName = 'Lavalink-Server' } -jar { +bootJar { archiveName = "Lavalink.jar" + + requiresUnpack '**/jda-nas*.jar' //otherwise we get missing classes exceptions +} + +bootRun { + //compiling tests during bootRun increases the likelyhood of catching broken tests locally instead of on the CI + dependsOn compileTestJava + + //pass in custom jvm args + // source: https://stackoverflow.com/a/25079415 + // example: ./gradlew bootRun -PjvmArgs="--illegal-access=debug -Dwhatever=value" + if (project.hasProperty('jvmArgs')) { + jvmArgs project.jvmArgs.split('\\s+') + } } -publishToMavenLocal.dependsOn 'bootRepackage' dependencies { - compile group: 'com.sedmelluq', name: 'lavaplayer', version: '1.2.47' - compile group: 'com.github.DV8FromTheWorld', name: 'JDA-Audio', version: '91438c36d7107cf838c2f2eb147b08f989d929db' - compile group: 'com.github.FredBoat', name: 'jda-nas', version: '1.0.6.1-JDA-Audio' - compile group: 'com.github.shredder121', name: 'jda-async-packetprovider', version: '1.1' - compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.3.7' - compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' - compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' - compile group: 'io.sentry', name: 'sentry-logback', version: '1.6.4' - compile group: 'com.github.oshi', name: 'oshi-core', version: '3.4.4' - compile group: 'org.json', name: 'json', version: '20180130' - compile group: 'com.google.guava', name: 'guava', version: '23.0' - compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.5.10.RELEASE' + compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion + compile group: 'com.github.DV8FromTheWorld', name: 'JDA-Audio', version: jdaAudioVersion + compile group: 'com.github.FredBoat', name: 'jda-nas', version: jdaNasVersion + compile group: 'com.github.shredder121', name: 'jda-async-packetprovider', version: jappVersion + compile group: 'org.java-websocket', name: 'Java-WebSocket', version: javaWebSocketVersion + compile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion + compile group: 'org.slf4j', name: 'slf4j-api', version: slf4jVersion + compile group: 'io.sentry', name: 'sentry-logback', version: sentryLogbackVersion + compile group: 'com.github.oshi', name: 'oshi-core', version: oshiVersion + compile group: 'org.json', name: 'json', version: jsonOrgVersion + compile group: 'com.google.guava', name: 'guava', version: guavaVersion + compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion +} + +//create a simple version file that we will be reading to create appropriate docker tags +void versionTxt() { + new File("$projectDir/VERSION.txt").text = "$project.version\n" } + +versionTxt() diff --git a/LavalinkServer/docker/.dockerignore b/LavalinkServer/docker/.dockerignore new file mode 100644 index 000000000..96800f5ce --- /dev/null +++ b/LavalinkServer/docker/.dockerignore @@ -0,0 +1,4 @@ +** +#ignore everything except the following files, they are used in the Dockerfile to build the fredboat image +!Dockerfile +!Lavalink.jar diff --git a/LavalinkServer/docker/Dockerfile b/LavalinkServer/docker/Dockerfile new file mode 100644 index 000000000..04e02e9bb --- /dev/null +++ b/LavalinkServer/docker/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:9-jre-slim + +WORKDIR /opt/Lavalink + +COPY Lavalink.jar Lavalink.jar + +ENTRYPOINT ["java", "-Xmx4G", "-jar", "Lavalink.jar"] diff --git a/LavalinkServer/src/main/java/lavalink/server/Launcher.java b/LavalinkServer/src/main/java/lavalink/server/Launcher.java index 6dfa659b6..aa353ed55 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Launcher.java +++ b/LavalinkServer/src/main/java/lavalink/server/Launcher.java @@ -26,40 +26,35 @@ import io.sentry.Sentry; import io.sentry.SentryClient; import io.sentry.logback.SentryAppender; -import lavalink.server.io.SocketContext; +import lavalink.server.config.ServerConfig; import lavalink.server.io.SocketServer; import lavalink.server.util.SimpleLogToSLF4JAdapter; import net.dv8tion.jda.utils.SimpleLog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Controller; import java.io.IOException; -import java.net.InetSocketAddress; import java.util.Properties; -@Configuration +@SpringBootApplication @ComponentScan -@EnableAutoConfiguration -@Controller public class Launcher { private static final Logger log = LoggerFactory.getLogger(Launcher.class); public final static long startTime = System.currentTimeMillis(); - public static Config config; - @SuppressWarnings("FieldCanBeLocal") - private final SocketServer socketServer; - @Autowired - public Launcher(Config config, SocketServer socketServer) { + public static void main(String[] args) { + SpringApplication sa = new SpringApplication(Launcher.class); + sa.setWebApplicationType(WebApplicationType.SERVLET); + sa.run(args); + } + + public Launcher(ServerConfig serverConfig, SocketServer socketServer) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { log.info("Shutdown hook triggered"); try { @@ -71,13 +66,11 @@ public Launcher(Config config, SocketServer socketServer) { SimpleLog.LEVEL = SimpleLog.Level.OFF; SimpleLog.addListener(new SimpleLogToSLF4JAdapter()); - Launcher.config = config; - initSentry(); - this.socketServer = socketServer; + initSentry(serverConfig); } - private void initSentry() { - String sentryDsn = config.getSentryDsn(); + private void initSentry(ServerConfig serverConfig) { + String sentryDsn = serverConfig.getSentryDsn(); if (sentryDsn == null || sentryDsn.isEmpty()) { log.info("No sentry dsn found, turning off sentry."); turnOffSentry(); @@ -109,42 +102,4 @@ private void turnOffSentry() { Sentry.close(); sentryAppender.stop(); } - - public static void main(String[] args) { - SpringApplication sa = new SpringApplication(Launcher.class); - sa.setWebEnvironment(true); - sa.run(args); - - String os = System.getProperty("os.name"); - - log.info("OS: " + System.getProperty("os.name") + ", Arch: " + System.getProperty("os.arch")); - - if ((os.contains("Windows") || os.contains("Linux")) - && !System.getProperty("os.arch").equalsIgnoreCase("arm") - && !System.getProperty("os.arch").equalsIgnoreCase("arm-linux") - ) { - SocketContext.nasSupported = true; - log.info("JDA-NAS supported system detected. Enabled native audio sending."); - - Integer customBuffer = config.getBufferDurationMs(); - if (customBuffer != null) { - log.info("Setting buffer to {}ms", customBuffer); - } else { - log.info("Using default buffer"); - } - } else { - log.warn("This system and architecture appears to not support native audio sending! " - + "GC pauses may cause your bot to stutter during playback."); - } - } - - @Bean - static SocketServer socketServer(@Value("${lavalink.server.ws.port:8080}") Integer port, - @Value("${lavalink.server.ws.host:0.0.0.0}") String host, - @Value("${lavalink.server.password}") String password) { - SocketServer ss = new SocketServer(new InetSocketAddress(host, port), password); - ss.start(); - return ss; - } - } diff --git a/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java new file mode 100644 index 000000000..78dacb5dd --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java @@ -0,0 +1,46 @@ +package lavalink.server.config; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * Created by napster on 05.03.18. + */ +@Component +public class AudioPlayerConfiguration { + + @Bean + public AudioPlayerManager audioPlayerManager(AudioSourcesConfig sources, ServerConfig serverConfig) { + + AudioPlayerManager audioPlayerManager = new DefaultAudioPlayerManager(); + audioPlayerManager.enableGcMonitoring(); + + if (sources.isYoutube()) { + YoutubeAudioSourceManager youtube = new YoutubeAudioSourceManager(); + Integer playlistLoadLimit = serverConfig.getYoutubePlaylistLoadLimit(); + + if (playlistLoadLimit != null) youtube.setPlaylistPageCount(playlistLoadLimit); + audioPlayerManager.registerSourceManager(youtube); + } + if (sources.isBandcamp()) audioPlayerManager.registerSourceManager(new BandcampAudioSourceManager()); + if (sources.isSoundcloud()) audioPlayerManager.registerSourceManager(new SoundCloudAudioSourceManager()); + if (sources.isTwitch()) audioPlayerManager.registerSourceManager(new TwitchStreamAudioSourceManager()); + if (sources.isVimeo()) audioPlayerManager.registerSourceManager(new VimeoAudioSourceManager()); + if (sources.isMixer()) audioPlayerManager.registerSourceManager(new BeamAudioSourceManager()); + if (sources.isHttp()) audioPlayerManager.registerSourceManager(new HttpAudioSourceManager()); + if (sources.isLocal()) audioPlayerManager.registerSourceManager(new LocalAudioSourceManager()); + + return audioPlayerManager; + } + +} diff --git a/LavalinkServer/src/main/java/lavalink/server/config/AudioSendFactoryConfiguration.java b/LavalinkServer/src/main/java/lavalink/server/config/AudioSendFactoryConfiguration.java new file mode 100644 index 000000000..31358ec89 --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/config/AudioSendFactoryConfiguration.java @@ -0,0 +1,56 @@ +package lavalink.server.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Created by napster on 05.03.18. + */ +@Component +public class AudioSendFactoryConfiguration { + + private static final Logger log = LoggerFactory.getLogger(AudioSendFactoryConfiguration.class); + + private boolean nasSupported = false; + private final int audioSendFactoryCount = Runtime.getRuntime().availableProcessors() * 2; + + public AudioSendFactoryConfiguration(ServerConfig serverConfig) { + String os = System.getProperty("os.name"); + + log.info("OS: " + System.getProperty("os.name") + ", Arch: " + System.getProperty("os.arch")); + + if ((os.contains("Windows") || os.contains("Linux")) + && !System.getProperty("os.arch").equalsIgnoreCase("arm") + && !System.getProperty("os.arch").equalsIgnoreCase("arm-linux") + ) { + nasSupported = true; + log.info("JDA-NAS supported system detected. Enabled native audio sending."); + + Integer customBuffer = serverConfig.getBufferDurationMs(); + if (customBuffer != null) { + log.info("Setting buffer to {}ms", customBuffer); + } else { + log.info("Using default buffer"); + } + + Integer customPlaylistLimit = serverConfig.getYoutubePlaylistLoadLimit(); + if (customPlaylistLimit != null) { + log.info("Setting playlist load limit to {}", customPlaylistLimit); + } else { + log.info("Using default playlist load limit"); + } + } else { + log.warn("This system and architecture appears to not support native audio sending! " + + "GC pauses may cause your bot to stutter during playback."); + } + } + + public boolean isNasSupported() { + return nasSupported; + } + + public int getAudioSendFactoryCount() { + return audioSendFactoryCount; + } +} diff --git a/LavalinkServer/src/main/java/lavalink/server/config/AudioSourcesConfig.java b/LavalinkServer/src/main/java/lavalink/server/config/AudioSourcesConfig.java new file mode 100644 index 000000000..cb28c0761 --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/config/AudioSourcesConfig.java @@ -0,0 +1,85 @@ +package lavalink.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Created by napster on 05.03.18. + */ +@ConfigurationProperties(prefix = "lavalink.server.sources") +@Component +public class AudioSourcesConfig { + + private boolean youtube = true; + private boolean bandcamp = true; + private boolean soundcloud = true; + private boolean twitch = true; + private boolean vimeo = true; + private boolean mixer = true; + private boolean http = true; + private boolean local = false; + + public boolean isYoutube() { + return youtube; + } + + public void setYoutube(boolean youtube) { + this.youtube = youtube; + } + + public boolean isBandcamp() { + return bandcamp; + } + + public void setBandcamp(boolean bandcamp) { + this.bandcamp = bandcamp; + } + + public boolean isSoundcloud() { + return soundcloud; + } + + public void setSoundcloud(boolean soundcloud) { + this.soundcloud = soundcloud; + } + + public boolean isTwitch() { + return twitch; + } + + public void setTwitch(boolean twitch) { + this.twitch = twitch; + } + + public boolean isVimeo() { + return vimeo; + } + + public void setVimeo(boolean vimeo) { + this.vimeo = vimeo; + } + + public boolean isMixer() { + return mixer; + } + + public void setMixer(boolean mixer) { + this.mixer = mixer; + } + + public boolean isHttp() { + return http; + } + + public void setHttp(boolean http) { + this.http = http; + } + + public boolean isLocal() { + return local; + } + + public void setLocal(boolean local) { + this.local = local; + } +} diff --git a/LavalinkServer/src/main/java/lavalink/server/Config.java b/LavalinkServer/src/main/java/lavalink/server/config/ServerConfig.java similarity index 52% rename from LavalinkServer/src/main/java/lavalink/server/Config.java rename to LavalinkServer/src/main/java/lavalink/server/config/ServerConfig.java index 4a1b8ea11..d3ec08e5c 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Config.java +++ b/LavalinkServer/src/main/java/lavalink/server/config/ServerConfig.java @@ -20,7 +20,7 @@ * SOFTWARE. */ -package lavalink.server; +package lavalink.server.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -29,13 +29,7 @@ @ConfigurationProperties(prefix = "lavalink.server") @Component -public class Config { - - private final Sources sources = new Sources(); - - public Sources getSources() { - return sources; - } +public class ServerConfig { private String password; @@ -47,19 +41,16 @@ public void setPassword(String password) { this.password = password; } - @Nullable - private String sentryDsn; + private String sentryDsn = ""; - @Nullable public String getSentryDsn() { return sentryDsn; } - public void setSentryDsn(@Nullable String sentryDsn) { + public void setSentryDsn(String sentryDsn) { this.sentryDsn = sentryDsn; } - @SuppressWarnings("WeakerAccess") @Nullable public Integer bufferDurationMs; @@ -83,80 +74,4 @@ public Integer getYoutubePlaylistLoadLimit() { public void setYoutubePlaylistLoadLimit(@Nullable Integer youtubePlaylistLoadLimit) { this.youtubePlaylistLoadLimit = youtubePlaylistLoadLimit; } - - public static class Sources { - - private boolean youtube = true; - private boolean bandcamp = true; - private boolean soundcloud = true; - private boolean twitch = true; - private boolean vimeo = true; - private boolean mixer = true; - private boolean http = true; - private boolean local = false; - - public boolean isYoutube() { - return youtube; - } - - public void setYoutube(boolean youtube) { - this.youtube = youtube; - } - - public boolean isBandcamp() { - return bandcamp; - } - - public void setBandcamp(boolean bandcamp) { - this.bandcamp = bandcamp; - } - - public boolean isSoundcloud() { - return soundcloud; - } - - public void setSoundcloud(boolean soundcloud) { - this.soundcloud = soundcloud; - } - - public boolean isTwitch() { - return twitch; - } - - public void setTwitch(boolean twitch) { - this.twitch = twitch; - } - - public boolean isVimeo() { - return vimeo; - } - - public void setVimeo(boolean vimeo) { - this.vimeo = vimeo; - } - - public boolean isMixer() { - return mixer; - } - - public void setMixer(boolean mixer) { - this.mixer = mixer; - } - - public boolean isHttp() { - return http; - } - - public void setHttp(boolean http) { - this.http = http; - } - - public boolean isLocal() { - return local; - } - - public void setLocal(boolean local) { - this.local = local; - } - } } diff --git a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java new file mode 100644 index 000000000..838619006 --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java @@ -0,0 +1,31 @@ +package lavalink.server.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Created by napster on 05.03.18. + */ +@ConfigurationProperties(prefix = "lavalink.server.ws") +@Component +public class WebsocketConfig { + + private int port = 80; + private String host = "0.0.0.0"; + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } +} diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index 17e0bd364..dea1078a0 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -24,7 +24,9 @@ import com.github.shredder121.asyncaudio.jdaaudio.AsyncPacketProviderFactory; import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory; -import lavalink.server.Launcher; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import lavalink.server.config.AudioSendFactoryConfiguration; +import lavalink.server.config.ServerConfig; import lavalink.server.player.Player; import lavalink.server.util.Util; import net.dv8tion.jda.Core; @@ -48,24 +50,30 @@ public class SocketContext { private static final Logger log = LoggerFactory.getLogger(SocketContext.class); - public static boolean nasSupported = false; + private final AudioPlayerManager audioPlayerManager; + private final ServerConfig serverConfig; private final WebSocket socket; + private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; private String userId; private int shardCount; private final Map cores = new HashMap<>(); private final Map players = new ConcurrentHashMap<>(); private ScheduledExecutorService statsExecutor; public final ScheduledExecutorService playerUpdateService; - private static final int audioSendFactoryCount = Runtime.getRuntime().availableProcessors() * 2; private final ConcurrentHashMap sendFactories = new ConcurrentHashMap<>(); - SocketContext(WebSocket socket, String userId, int shardCount) { + SocketContext(AudioPlayerManager audioPlayerManager, ServerConfig serverConfig, WebSocket socket, + AudioSendFactoryConfiguration audioSendFactoryConfiguration, SocketServer socketServer, + String userId, int shardCount) { + this.audioPlayerManager = audioPlayerManager; + this.serverConfig = serverConfig; this.socket = socket; + this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; this.userId = userId; this.shardCount = shardCount; statsExecutor = Executors.newSingleThreadScheduledExecutor(); - statsExecutor.scheduleAtFixedRate(new StatsTask(this), 0, 1, TimeUnit.MINUTES); + statsExecutor.scheduleAtFixedRate(new StatsTask(this, socketServer), 0, 1, TimeUnit.MINUTES); playerUpdateService = Executors.newScheduledThreadPool(2, r -> { Thread thread = new Thread(r); @@ -78,7 +86,7 @@ public class SocketContext { Core getCore(int shardId) { return cores.computeIfAbsent(shardId, __ -> { - if (nasSupported) + if (audioSendFactoryConfiguration.isNasSupported()) return new Core(userId, new CoreClientImpl(), core -> new ConnectionManagerImpl(), getAudioSendFactory(shardId)); else return new Core(userId, new CoreClientImpl(), (ConnectionManagerBuilder) core -> new ConnectionManagerImpl()); @@ -88,7 +96,7 @@ Core getCore(int shardId) { Player getPlayer(String guildId) { return players.computeIfAbsent(guildId, - __ -> new Player(this, guildId) + __ -> new Player(this, guildId, audioPlayerManager) ); } @@ -130,17 +138,18 @@ void shutdown() { } private IAudioSendFactory getAudioSendFactory(int shardId) { - return sendFactories.computeIfAbsent(shardId % audioSendFactoryCount, integer -> { - Integer customBuffer = Launcher.config.getBufferDurationMs(); - NativeAudioSendFactory nativeAudioSendFactory; - if (customBuffer != null) { - nativeAudioSendFactory = new NativeAudioSendFactory(customBuffer); - } else { - nativeAudioSendFactory = new NativeAudioSendFactory(); - } - - return AsyncPacketProviderFactory.adapt(nativeAudioSendFactory); - }); + return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.getAudioSendFactoryCount(), + integer -> { + Integer customBuffer = serverConfig.getBufferDurationMs(); + NativeAudioSendFactory nativeAudioSendFactory; + if (customBuffer != null) { + nativeAudioSendFactory = new NativeAudioSendFactory(customBuffer); + } else { + nativeAudioSendFactory = new NativeAudioSendFactory(); + } + + return AsyncPacketProviderFactory.adapt(nativeAudioSendFactory); + }); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index 28d32333c..066950baa 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -22,8 +22,12 @@ package lavalink.server.io; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.TrackMarker; +import lavalink.server.config.AudioSendFactoryConfiguration; +import lavalink.server.config.ServerConfig; +import lavalink.server.config.WebsocketConfig; import lavalink.server.player.Player; import lavalink.server.player.TrackEndMarkerHandler; import lavalink.server.util.Util; @@ -35,7 +39,9 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; import java.io.IOException; import java.net.InetSocketAddress; import java.util.Collection; @@ -45,15 +51,28 @@ import static lavalink.server.io.WSCodes.AUTHORIZATION_REJECTED; import static lavalink.server.io.WSCodes.INTERNAL_ERROR; +@Component public class SocketServer extends WebSocketServer { private static final Logger log = LoggerFactory.getLogger(SocketServer.class); - private static final Map contextMap = new HashMap<>(); - private final String password; - public SocketServer(InetSocketAddress address, String password) { - super(address); - this.password = password; + private final Map contextMap = new HashMap<>(); + private final ServerConfig serverConfig; + private final AudioPlayerManager audioPlayerManager; + private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; + + public SocketServer(WebsocketConfig websocketConfig, ServerConfig serverConfig, AudioPlayerManager audioPlayerManager, + AudioSendFactoryConfiguration audioSendFactoryConfiguration) { + super(new InetSocketAddress(websocketConfig.getHost(), websocketConfig.getPort())); + this.serverConfig = serverConfig; + this.audioPlayerManager = audioPlayerManager; + this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; + } + + @Override + @PostConstruct + public void start() { + super.start(); } @Override @@ -62,9 +81,10 @@ public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) { int shardCount = Integer.parseInt(clientHandshake.getFieldValue("Num-Shards")); String userId = clientHandshake.getFieldValue("User-Id"); - if (clientHandshake.getFieldValue("Authorization").equals(password)) { + if (clientHandshake.getFieldValue("Authorization").equals(serverConfig.getPassword())) { log.info("Connection opened from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); - contextMap.put(webSocket, new SocketContext(webSocket, userId, shardCount)); + contextMap.put(webSocket, new SocketContext(audioPlayerManager, serverConfig, webSocket, + audioSendFactoryConfiguration, this, userId, shardCount)); } else { log.error("Authentication failed from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); webSocket.close(AUTHORIZATION_REJECTED, "Authorization rejected"); @@ -126,7 +146,7 @@ public void onMessage(WebSocket webSocket, String s) { case "play": try { Player player = contextMap.get(webSocket).getPlayer(json.getString("guildId")); - AudioTrack track = Util.toAudioTrack(json.getString("track")); + AudioTrack track = Util.toAudioTrack(audioPlayerManager, json.getString("track")); if (json.has("startTime")) { track.setPosition(json.getLong("startTime")); } @@ -168,10 +188,11 @@ public void onMessage(WebSocket webSocket, String s) { case "destroy": Player player5 = contextMap.get(webSocket).getPlayers().remove(json.getString("guildId")); if (player5 != null) player5.stop(); - contextMap.get(webSocket) + AudioManager audioManager = contextMap.get(webSocket) .getCore(getShardId(webSocket, json)) - .getAudioManager(json.getString("guildId")) - .closeAudioConnection(); + .getAudioManager(json.getString("guildId")); + audioManager.setSendingHandler(null); + audioManager.closeAudioConnection(); break; default: log.warn("Unexpected operation: " + json.getString("op")); @@ -203,7 +224,7 @@ private int getShardId(WebSocket webSocket, JSONObject json) { return Util.getShardFromSnowflake(json.getString("guildId"), contextMap.get(webSocket).getShardCount()); } - static Collection getConnections() { + Collection getConnections() { return contextMap.values(); } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java index 941a4a1c7..c065676c0 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java @@ -38,11 +38,13 @@ public class StatsTask implements Runnable { private static final Logger log = LoggerFactory.getLogger(StatsTask.class); private SocketContext context; + private final SocketServer socketServer; private final SystemInfo si = new SystemInfo(); - StatsTask(SocketContext context) { + StatsTask(SocketContext context, SocketServer socketServer) { this.context = context; + this.socketServer = socketServer; } @Override @@ -60,7 +62,7 @@ public void sendStats() { final int[] playersTotal = {0}; final int[] playersPlaying = {0}; - SocketServer.getConnections().forEach(socketContext -> { + socketServer.getConnections().forEach(socketContext -> { playersTotal[0] += socketContext.getPlayers().size(); playersPlaying[0] += socketContext.getPlayingPlayers().size(); }); diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java index be4c5481d..272252a3b 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java @@ -23,6 +23,7 @@ package lavalink.server.player; import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -35,17 +36,22 @@ public class AudioLoader implements AudioLoadResultHandler { private static final Logger log = LoggerFactory.getLogger(AudioLoader.class); + private final AudioPlayerManager audioPlayerManager; private List loadedItems; private boolean used = false; + public AudioLoader(AudioPlayerManager audioPlayerManager) { + this.audioPlayerManager = audioPlayerManager; + } + List loadSync(String identifier) throws InterruptedException { if(used) throw new IllegalStateException("This loader can only be used once per instance"); used = true; - Player.PLAYER_MANAGER.loadItem(identifier, this); + audioPlayerManager.loadItem(identifier, this); synchronized (this) { this.wait(); diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java index 838fb3bb4..040df6188 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java @@ -22,9 +22,10 @@ package lavalink.server.player; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; -import lavalink.server.Launcher; +import lavalink.server.config.ServerConfig; import lavalink.server.util.Util; import org.json.JSONArray; import org.json.JSONObject; @@ -46,6 +47,13 @@ public class AudioLoaderRestHandler { private static final Logger log = LoggerFactory.getLogger(AudioLoaderRestHandler.class); + private final AudioPlayerManager audioPlayerManager; + private final ServerConfig serverConfig; + + public AudioLoaderRestHandler(AudioPlayerManager audioPlayerManager, ServerConfig serverConfig) { + this.audioPlayerManager = audioPlayerManager; + this.serverConfig = serverConfig; + } private void log(HttpServletRequest request) { String path = request.getServletPath(); @@ -58,7 +66,7 @@ private boolean isAuthorized(HttpServletRequest request, HttpServletResponse res return false; } - if (!request.getHeader("Authorization").equals(Launcher.config.getPassword())) { + if (!request.getHeader("Authorization").equals(serverConfig.getPassword())) { log.warn("Authorization failed"); response.setStatus(403); return false; @@ -90,14 +98,14 @@ public String getLoadTracks(HttpServletRequest request, HttpServletResponse resp return ""; JSONArray tracks = new JSONArray(); - List list = new AudioLoader().loadSync(identifier); + List list = new AudioLoader(audioPlayerManager).loadSync(identifier); list.forEach(track -> { JSONObject object = new JSONObject(); object.put("info", trackToJSON(track)); try { - String encoded = Util.toMessage(track); + String encoded = Util.toMessage(audioPlayerManager, track); object.put("track", encoded); tracks.put(object); } catch (IOException e) { @@ -116,7 +124,7 @@ public String getDecodeTrack(HttpServletRequest request, HttpServletResponse res if (!isAuthorized(request, response)) return ""; - AudioTrack audioTrack = Util.toAudioTrack(track); + AudioTrack audioTrack = Util.toAudioTrack(audioPlayerManager, track); return trackToJSON(audioTrack).toString(); } @@ -134,7 +142,7 @@ public String postDecodeTracks(HttpServletRequest request, HttpServletResponse r for (int i = 0; i < requestJSON.length(); i++) { String track = requestJSON.getString(i); - AudioTrack audioTrack = Util.toAudioTrack(track); + AudioTrack audioTrack = Util.toAudioTrack(audioPlayerManager, track); JSONObject infoJSON = trackToJSON(audioTrack); JSONObject trackJSON = new JSONObject() diff --git a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java index 3d3cc0b69..336bf59c0 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/EventEmitter.java @@ -23,6 +23,7 @@ package lavalink.server.player; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; @@ -38,9 +39,11 @@ public class EventEmitter extends AudioEventAdapter { private static final Logger log = LoggerFactory.getLogger(EventEmitter.class); + private final AudioPlayerManager audioPlayerManager; private final Player linkPlayer; - EventEmitter(Player linkPlayer) { + EventEmitter(AudioPlayerManager audioPlayerManager, Player linkPlayer) { + this.audioPlayerManager = audioPlayerManager; this.linkPlayer = linkPlayer; } @@ -51,7 +54,7 @@ public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason out.put("type", "TrackEndEvent"); out.put("guildId", linkPlayer.getGuildId()); try { - out.put("track", Util.toMessage(track)); + out.put("track", Util.toMessage(audioPlayerManager, track)); } catch (IOException e) { out.put("track", JSONObject.NULL); } @@ -69,7 +72,7 @@ public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyExcep out.put("type", "TrackExceptionEvent"); out.put("guildId", linkPlayer.getGuildId()); try { - out.put("track", Util.toMessage(track)); + out.put("track", Util.toMessage(audioPlayerManager, track)); } catch (IOException e) { out.put("track", JSONObject.NULL); } @@ -88,7 +91,7 @@ public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) out.put("type", "TrackStuckEvent"); out.put("guildId", linkPlayer.getGuildId()); try { - out.put("track", Util.toMessage(track)); + out.put("track", Util.toMessage(audioPlayerManager, track)); } catch (IOException e) { out.put("track", JSONObject.NULL); } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index 6c46b41a9..53f14a3f4 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -24,21 +24,10 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; -import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; -import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager; -import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; -import lavalink.server.Config; -import lavalink.server.Launcher; import lavalink.server.io.SocketContext; import lavalink.server.io.SocketServer; import net.dv8tion.jda.audio.AudioSendHandler; @@ -53,29 +42,6 @@ public class Player extends AudioEventAdapter implements AudioSendHandler { private static final Logger log = LoggerFactory.getLogger(Player.class); - public static final AudioPlayerManager PLAYER_MANAGER; - - static { - PLAYER_MANAGER = new DefaultAudioPlayerManager(); - PLAYER_MANAGER.enableGcMonitoring(); - - Config.Sources sources = Launcher.config.getSources(); - if (sources.isYoutube()) { - YoutubeAudioSourceManager youtube = new YoutubeAudioSourceManager(); - Integer playlistLoadLimit = Launcher.config.getYoutubePlaylistLoadLimit(); - - if (playlistLoadLimit != null) youtube.setPlaylistPageCount(playlistLoadLimit); - PLAYER_MANAGER.registerSourceManager(new YoutubeAudioSourceManager()); - } - if (sources.isBandcamp()) PLAYER_MANAGER.registerSourceManager(new BandcampAudioSourceManager()); - if (sources.isSoundcloud()) PLAYER_MANAGER.registerSourceManager(new SoundCloudAudioSourceManager()); - if (sources.isTwitch()) PLAYER_MANAGER.registerSourceManager(new TwitchStreamAudioSourceManager()); - if (sources.isVimeo()) PLAYER_MANAGER.registerSourceManager(new VimeoAudioSourceManager()); - if (sources.isMixer()) PLAYER_MANAGER.registerSourceManager(new BeamAudioSourceManager()); - if (sources.isHttp()) PLAYER_MANAGER.registerSourceManager(new HttpAudioSourceManager()); - if (sources.isLocal()) PLAYER_MANAGER.registerSourceManager(new LocalAudioSourceManager()); - } - private SocketContext socketContext; private final String guildId; private final AudioPlayer player; @@ -83,12 +49,12 @@ public class Player extends AudioEventAdapter implements AudioSendHandler { private AudioFrame lastFrame = null; private ScheduledFuture myFuture = null; - public Player(SocketContext socketContext, String guildId) { + public Player(SocketContext socketContext, String guildId, AudioPlayerManager audioPlayerManager) { this.socketContext = socketContext; this.guildId = guildId; - this.player = PLAYER_MANAGER.createPlayer(); + this.player = audioPlayerManager.createPlayer(); this.player.addListener(this); - this.player.addListener(new EventEmitter(this)); + this.player.addListener(new EventEmitter(audioPlayerManager, this)); this.player.addListener(audioLossCounter); } diff --git a/LavalinkServer/src/main/java/lavalink/server/util/Util.java b/LavalinkServer/src/main/java/lavalink/server/util/Util.java index 836673027..67ff7eb63 100644 --- a/LavalinkServer/src/main/java/lavalink/server/util/Util.java +++ b/LavalinkServer/src/main/java/lavalink/server/util/Util.java @@ -22,10 +22,10 @@ package lavalink.server.util; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.tools.io.MessageInput; import com.sedmelluq.discord.lavaplayer.tools.io.MessageOutput; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; -import lavalink.server.player.Player; import org.apache.commons.codec.binary.Base64; import java.io.ByteArrayInputStream; @@ -38,15 +38,15 @@ public static int getShardFromSnowflake(String snowflake, int numShards) { return (int) ((Long.parseLong(snowflake) >> 22) % numShards); } - public static AudioTrack toAudioTrack(String message) throws IOException { + public static AudioTrack toAudioTrack(AudioPlayerManager audioPlayerManager, String message) throws IOException { byte[] b64 = Base64.decodeBase64(message); ByteArrayInputStream bais = new ByteArrayInputStream(b64); - return Player.PLAYER_MANAGER.decodeTrack(new MessageInput(bais)).decodedTrack; + return audioPlayerManager.decodeTrack(new MessageInput(bais)).decodedTrack; } - public static String toMessage(AudioTrack track) throws IOException { + public static String toMessage(AudioPlayerManager audioPlayerManager, AudioTrack track) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Player.PLAYER_MANAGER.encodeTrack(new MessageOutput(baos), track); + audioPlayerManager.encodeTrack(new MessageOutput(baos), track); return Base64.encodeBase64String(baos.toByteArray()); } diff --git a/build.gradle b/build.gradle index c37f11784..a7de18987 100644 --- a/build.gradle +++ b/build.gradle @@ -11,38 +11,59 @@ allprojects { } subprojects { + buildscript { + ext { + springBootVersion = '2.0.0.RELEASE' + gradleGitVersion = '1.4.21' + } + repositories { + maven { url "https://plugins.gradle.org/m2/" } + maven { url 'http://repo.spring.io/plugins-release' } + } + dependencies { + classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:${gradleGitVersion}" + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" + } + } apply plugin: 'java' apply plugin: 'idea' - apply plugin: 'maven-publish' sourceCompatibility = 1.8 targetCompatibility = 1.8 - publishing { - publications { - mavenJava(MavenPublication) { - groupId rootProject.group - artifactId moduleName - - from components.java - - artifact sourceJar { - classifier "sources" - } - } - } - } - compileJava.dependsOn 'clean' compileJava.options.encoding = 'UTF-8' compileJava.options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - task install(dependsOn: 'publishToMavenLocal') - publishToMavenLocal.dependsOn 'jar' - task sourceJar(type: Jar) { from sourceSets.main.allJava } + + ext { + //@formatter:off + + lavaplayerVersion = '1.2.58' + jdaAudioVersion = '91438c36d7107cf838c2f2eb147b08f989d929db' + jdaNasVersion = '1.0.6.1-JDA-Audio' + jappVersion = '1.2' + jdaVersion = '3.5.1_345' + + springBootVersion = "${springBootVersion}" + javaWebSocketVersion = '1.3.7' + logbackVersion = '1.2.3' + slf4jVersion = '1.7.25' + sentryLogbackVersion = '1.7.0' + oshiVersion = '3.4.4' + jsonOrgVersion = '20180130' + guavaVersion = '24.0-jre' + prometheusVersion = '0.3.0' + + junitJupiterVersion = '5.1.0' + junitPlatformVersion = '1.1.0' + unirestVersion = '1.4.9' + + //@formatter:on + } } ext { @@ -52,6 +73,6 @@ ext { import org.gradle.api.tasks.wrapper.Wrapper.DistributionType task wrapper(type: Wrapper) { - gradleVersion = '4.4.1' + gradleVersion = '4.6' distributionType = DistributionType.ALL } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ed88a042a..f6b961fd5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 57c7d2d22..9a4163a4f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip