>) {
+ val builder = Jackson2ObjectMapperBuilder()
+ builder.configure(objectMapper())
+ converters.add(StringHttpMessageConverter())
+ converters.add(MappingJackson2HttpMessageConverter(builder.build()))
+ }
+
+ @Bean
+ fun jackson2ObjectMapperBuilder(): Jackson2ObjectMapperBuilder {
+ val builder = Jackson2ObjectMapperBuilder()
+ builder.configure(objectMapper())
+ return builder
+ }
+
+ override fun addInterceptors(registry: InterceptorRegistry) {
+ interceptors.forEach { registry.addInterceptor(it) }
+ }
+
+}
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java
deleted file mode 100644
index 105de19fd..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package lavalink.server.config;
-
-import lavalink.server.io.HandshakeInterceptorImpl;
-import lavalink.server.io.SocketServer;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.socket.config.annotation.EnableWebSocket;
-import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
-import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
-
-@Configuration
-@EnableWebSocket
-public class WebsocketConfig implements WebSocketConfigurer {
-
- private final SocketServer server;
- private final HandshakeInterceptorImpl handshakeInterceptor;
-
- @Autowired
- public WebsocketConfig(SocketServer server, HandshakeInterceptorImpl handshakeInterceptor) {
- this.server = server;
- this.handshakeInterceptor = handshakeInterceptor;
- }
-
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(server, "/")
- .addInterceptors(handshakeInterceptor);
- }
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.kt b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.kt
new file mode 100644
index 000000000..e95b84d34
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/config/WebsocketConfig.kt
@@ -0,0 +1,19 @@
+package lavalink.server.config
+
+import lavalink.server.io.HandshakeInterceptorImpl
+import lavalink.server.io.SocketServer
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.socket.config.annotation.EnableWebSocket
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
+
+@Configuration
+@EnableWebSocket
+class WebsocketConfig(
+ private val server: SocketServer,
+ private val handshakeInterceptor: HandshakeInterceptorImpl,
+) : WebSocketConfigurer {
+ override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
+ registry.addHandler(server, "/", "/v3/websocket").addInterceptors(handshakeInterceptor)
+ }
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/config/YoutubeConfig.kt b/LavalinkServer/src/main/java/lavalink/server/config/YoutubeConfig.kt
index 8462fb468..1c3139d7c 100644
--- a/LavalinkServer/src/main/java/lavalink/server/config/YoutubeConfig.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/config/YoutubeConfig.kt
@@ -1,6 +1,6 @@
package lavalink.server.config
data class YoutubeConfig(
- var email: String = "",
- var password: String = ""
+ var email: String = "",
+ var password: String = ""
)
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java
deleted file mode 100644
index b2898fd42..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package lavalink.server.info;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Component;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Properties;
-
-/**
- * Created by napster on 25.06.18.
- *
- * Requires app.properties to be populated with values during the gradle build
- */
-@Component
-public class AppInfo {
-
- private static final Logger log = LoggerFactory.getLogger(AppInfo.class);
-
- private final String version;
- private final String groupId;
- private final String artifactId;
- private final long buildTime;
-
- public AppInfo() {
- InputStream resourceAsStream = this.getClass().getResourceAsStream("/app.properties");
- Properties prop = new Properties();
- try {
- prop.load(resourceAsStream);
- } catch (IOException e) {
- log.error("Failed to load app.properties", e);
- }
- this.version = prop.getProperty("version");
- this.groupId = prop.getProperty("groupId");
- this.artifactId = prop.getProperty("artifactId");
- long bTime = -1L;
- try {
- bTime = Long.parseLong(prop.getProperty("buildTime"));
- } catch (NumberFormatException ignored) { }
- this.buildTime = bTime;
- }
-
- public String getVersion() {
- return this.version;
- }
-
- public String getGroupId() {
- return this.groupId;
- }
-
- public String getArtifactId() {
- return this.artifactId;
- }
-
- public long getBuildTime() {
- return this.buildTime;
- }
-
- public String getVersionBuild() {
- return this.version;
- }
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.kt b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.kt
new file mode 100644
index 000000000..21e746601
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.kt
@@ -0,0 +1,38 @@
+package lavalink.server.info
+
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import java.io.IOException
+import java.util.*
+
+/**
+ * Created by napster on 25.06.18.
+ *
+ * Requires app.properties to be populated with values during the gradle build
+ */
+@Component
+class AppInfo {
+ companion object {
+ private val log = LoggerFactory.getLogger(AppInfo::class.java)
+ }
+
+ final val versionBuild: String
+ final val groupId: String
+ final val artifactId: String
+ final val buildTime: Long
+
+ init {
+ val resourceAsStream = this.javaClass.getResourceAsStream("/app.properties")
+ val prop = Properties()
+ try {
+ prop.load(resourceAsStream)
+ } catch (e: IOException) {
+ log.error("Failed to load app.properties", e)
+ }
+
+ versionBuild = prop.getProperty("version")
+ groupId = prop.getProperty("groupId")
+ artifactId = prop.getProperty("artifactId")
+ buildTime = prop.getProperty("buildTime").toLongOrNull() ?: -1
+ }
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java
deleted file mode 100644
index 600282436..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package lavalink.server.info;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Component;
-
-import java.io.IOException;
-import java.time.OffsetDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Properties;
-
-/**
- * Created by napster on 25.06.18.
- *
- * Provides access to the values of the property file generated by whatever git info plugin we're using
- *
- * Requires a generated git.properties, which can be achieved with the gradle git plugin
- */
-@Component
-public class GitRepoState {
-
- private static final Logger log = LoggerFactory.getLogger(GitRepoState.class);
-
- private boolean loaded = false;
- private final String branch;
- private final String commitId;
- private final String commitIdAbbrev;
- private final String commitUserName;
- private final String commitUserEmail;
- private final String commitMessageFull;
- private final String commitMessageShort;
- private final long commitTime; //epoch seconds
-
- @SuppressWarnings("ConstantConditions")
- public GitRepoState() {
-
- Properties properties = new Properties();
- try {
- properties.load(GitRepoState.class.getClassLoader().getResourceAsStream("git.properties"));
- loaded = true;
- } catch (NullPointerException e) {
- log.trace("Failed to load git repo information. Did you build with the git gradle plugin? Is the git.properties file present?");
- } catch (IOException e) {
- log.info("Failed to load git repo information due to suspicious IOException", e);
- }
-
- this.branch = String.valueOf(properties.getOrDefault("git.branch", ""));
- this.commitId = String.valueOf(properties.getOrDefault("git.commit.id", ""));
- this.commitIdAbbrev = String.valueOf(properties.getOrDefault("git.commit.id.abbrev", ""));
- this.commitUserName = String.valueOf(properties.getOrDefault("git.commit.user.name", ""));
- this.commitUserEmail = String.valueOf(properties.getOrDefault("git.commit.user.email", ""));
- this.commitMessageFull = String.valueOf(properties.getOrDefault("git.commit.message.full", ""));
- this.commitMessageShort = String.valueOf(properties.getOrDefault("git.commit.message.short", ""));
- final String time = String.valueOf(properties.get("git.commit.time"));
- if (time == null || time.equals("null")) {
- this.commitTime = 0;
- } else {
- // https://github.com/n0mer/gradle-git-properties/issues/71
- DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
- this.commitTime = OffsetDateTime.from(dtf.parse(time)).toEpochSecond();
- }
- }
-
- public String getBranch() {
- return this.branch;
- }
-
- public String getCommitId() {
- return this.commitId;
- }
-
- public String getCommitIdAbbrev() {
- return this.commitIdAbbrev;
- }
-
- public String getCommitUserName() {
- return this.commitUserName;
- }
-
- public String getCommitUserEmail() {
- return this.commitUserEmail;
- }
-
- public String getCommitMessageFull() {
- return this.commitMessageFull;
- }
-
- public String getCommitMessageShort() {
- return this.commitMessageShort;
- }
-
- /**
- * @return commit time in epoch seconds
- */
- public long getCommitTime() {
- return this.commitTime;
- }
-
- public boolean isLoaded() {
- return loaded;
- }
-}
-
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.kt b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.kt
new file mode 100644
index 000000000..415e4f5a8
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.kt
@@ -0,0 +1,65 @@
+package lavalink.server.info
+
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import java.io.IOException
+import java.time.OffsetDateTime
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+/**
+ * Created by napster on 25.06.18.
+ *
+ * Provides access to the values of the property file generated by whatever git info plugin we're using*
+ *
+ * Requires a generated git.properties, which can be achieved with the gradle git plugin
+ */
+@Component
+class GitRepoState {
+ companion object {
+ private val log = LoggerFactory.getLogger(GitRepoState::class.java)
+ }
+
+ /**
+ * Commit time in epoch seconds
+ */
+ final val commitTime: Long
+ final val branch: String
+ final val commitId: String
+ final val commitIdAbbrev: String
+ final val commitUserName: String
+ final val commitUserEmail: String
+ final val commitMessageFull: String
+ final val commitMessageShort: String
+
+ final var isLoaded = false
+
+ init {
+ val properties = Properties()
+ try {
+ properties.load(GitRepoState::class.java.classLoader.getResourceAsStream("git.properties"))
+ isLoaded = true
+ } catch (e: NullPointerException) {
+ log.trace("Failed to load git repo information. Did you build with the git gradle plugin? Is the git.properties file present?")
+ } catch (e: IOException) {
+ log.info("Failed to load git repo information due to suspicious IOException", e)
+ }
+
+ branch = properties.getOrDefault("git.branch", "").toString()
+ commitId = properties.getOrDefault("git.commit.id", "").toString()
+ commitIdAbbrev = properties.getOrDefault("git.commit.id.abbrev", "").toString()
+ commitUserName = properties.getOrDefault("git.commit.user.name", "").toString()
+ commitUserEmail = properties.getOrDefault("git.commit.user.email", "").toString()
+ commitMessageFull = properties.getOrDefault("git.commit.message.full", "").toString()
+ commitMessageShort = properties.getOrDefault("git.commit.message.short", "").toString()
+
+ val time = properties["git.commit.time"].toString()
+ commitTime = if (time == "null") {
+ 0
+ } else {
+ // https://github.com/n0mer/gradle-git-properties/issues/71
+ val dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")
+ OffsetDateTime.from(dtf.parse(time)).toEpochSecond()
+ }
+ }
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.java b/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.java
deleted file mode 100644
index 8cd870707..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package lavalink.server.info;
-
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * Created by napster on 08.03.19.
- */
-@RestController
-public class InfoRestHandler {
-
- private final AppInfo appInfo;
-
- public InfoRestHandler(AppInfo appInfo) {
- this.appInfo = appInfo;
- }
-
- @GetMapping("/version")
- public String version() {
- return appInfo.getVersionBuild();
- }
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.kt b/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.kt
new file mode 100644
index 000000000..00bbc9b6e
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/info/InfoRestHandler.kt
@@ -0,0 +1,59 @@
+package lavalink.server.info
+
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager
+import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary
+import dev.arbjerg.lavalink.api.AudioFilterExtension
+import dev.arbjerg.lavalink.protocol.v3.*
+import lavalink.server.bootstrap.PluginManager
+import lavalink.server.config.ServerConfig
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+
+/**
+ * Created by napster on 08.03.19.
+ */
+@RestController
+class InfoRestHandler(
+ appInfo: AppInfo,
+ gitRepoState: GitRepoState,
+ audioPlayerManager: AudioPlayerManager,
+ pluginManager: PluginManager,
+ serverConfig: ServerConfig,
+ filterExtensions: List
+) {
+
+ private val enabledFilers = (listOf(
+ "volume",
+ "equalizer",
+ "karaoke",
+ "timescale",
+ "tremolo",
+ "vibrato",
+ "distortion",
+ "rotation",
+ "channelMix",
+ "lowPass"
+ ) + filterExtensions.map { it.name }).filter {
+ it !in serverConfig.filters || serverConfig.filters[it] == true
+ }
+
+ private val info = Info(
+ Version.fromSemver(appInfo.versionBuild),
+ appInfo.buildTime,
+ Git(gitRepoState.branch, gitRepoState.commitIdAbbrev, gitRepoState.commitTime * 1000),
+ System.getProperty("java.version"),
+ PlayerLibrary.VERSION,
+ audioPlayerManager.sourceManagers.map { it.sourceName },
+ enabledFilers,
+ Plugins(pluginManager.pluginManifests.map {
+ Plugin(it.name, it.version)
+ })
+ )
+ private val version = appInfo.versionBuild
+
+ @GetMapping("/v3/info")
+ fun info() = info
+
+ @GetMapping("/version")
+ fun version() = version
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/EventEmitter.kt b/LavalinkServer/src/main/java/lavalink/server/io/EventEmitter.kt
index 82a4285bc..522f48fea 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/EventEmitter.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/EventEmitter.kt
@@ -16,10 +16,10 @@ class EventEmitter(private val context: SocketContext, private val listeners: Co
fun onSocketContextDestroyed() = iterate { it.onSocketContextDestroyed(context) }
fun onWebsocketMessageIn(message: String) = iterate { it.onWebsocketMessageIn(context, message) }
fun onWebSocketMessageOut(message: String) = iterate { it.onWebSocketMessageOut(context, message) }
- fun onNewPlayer(player: IPlayer) = iterate { it.onNewPlayer(context, player) }
- fun onDestroyPlayer(player: IPlayer) = iterate { it.onDestroyPlayer(context, player) }
+ fun onNewPlayer(player: IPlayer) = iterate { it.onNewPlayer(context, player) }
+ fun onDestroyPlayer(player: IPlayer) = iterate { it.onDestroyPlayer(context, player) }
- private fun iterate(func: (PluginEventHandler) -> Unit ) {
+ private fun iterate(func: (PluginEventHandler) -> Unit) {
listeners.forEach {
try {
func(it)
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.kt b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.kt
index b15dd5a43..805c1f196 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/HandshakeInterceptorImpl.kt
@@ -23,27 +23,38 @@ constructor(private val serverConfig: ServerConfig, private val socketServer: So
*
* @return true if authenticated
*/
- override fun beforeHandshake(request: ServerHttpRequest, response: ServerHttpResponse, wsHandler: WebSocketHandler,
- attributes: Map): Boolean {
+ @Suppress("UastIncorrectHttpHeaderInspection")
+ override fun beforeHandshake(
+ request: ServerHttpRequest, response: ServerHttpResponse, wsHandler: WebSocketHandler,
+ attributes: Map
+ ): Boolean {
val password = request.headers.getFirst("Authorization")
- val matches = password == serverConfig.password
- if (matches) {
- log.info("Incoming connection from " + request.remoteAddress)
- } else {
- log.error("Authentication failed from " + request.remoteAddress)
+ if (password != serverConfig.password) {
+ log.error("Authentication failed from ${request.remoteAddress}")
response.setStatusCode(HttpStatus.UNAUTHORIZED)
+ return false
}
+ if (request.headers.getFirst("User-Id") == null) {
+ log.error("Missing User-Id header from ${request.remoteAddress}")
+ response.setStatusCode(HttpStatus.BAD_REQUEST)
+ return false
+ }
+
+ log.info("Incoming connection from ${request.remoteAddress}")
+
val resumeKey = request.headers.getFirst("Resume-Key")
val resuming = resumeKey != null && socketServer.canResume(resumeKey)
response.headers.add("Session-Resumed", resuming.toString())
- return matches
+ return true
}
// No action required
- override fun afterHandshake(request: ServerHttpRequest, response: ServerHttpResponse, wsHandler: WebSocketHandler,
- exception: Exception?) {
+ override fun afterHandshake(
+ request: ServerHttpRequest, response: ServerHttpResponse, wsHandler: WebSocketHandler,
+ exception: Exception?
+ ) {
}
}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/PluginsEndpoint.kt b/LavalinkServer/src/main/java/lavalink/server/io/PluginsEndpoint.kt
index f65ade11b..15de43a1a 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/PluginsEndpoint.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/PluginsEndpoint.kt
@@ -1,5 +1,7 @@
package lavalink.server.io
+import dev.arbjerg.lavalink.protocol.v3.Plugin
+import dev.arbjerg.lavalink.protocol.v3.Plugins
import lavalink.server.bootstrap.PluginManager
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@@ -7,14 +9,9 @@ import org.springframework.web.bind.annotation.RestController
@RestController
class PluginsEndpoint(pluginManager: PluginManager) {
- private val data = pluginManager.pluginManifests.map {
- mutableMapOf().apply {
- put("name", it.name)
- put("version", it.version)
- }
- }
+ private val plugins = Plugins(pluginManager.pluginManifests.map { Plugin(it.name, it.version) })
@GetMapping("/plugins")
- fun plugins() = data
+ fun plugins() = plugins
-}
\ No newline at end of file
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/RequestLoggingFilter.kt b/LavalinkServer/src/main/java/lavalink/server/io/RequestLoggingFilter.kt
new file mode 100644
index 000000000..78ac57143
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/io/RequestLoggingFilter.kt
@@ -0,0 +1,31 @@
+package lavalink.server.io
+
+import lavalink.server.config.RequestLoggingConfig
+import org.slf4j.LoggerFactory
+import org.springframework.web.filter.AbstractRequestLoggingFilter
+import javax.servlet.http.HttpServletRequest
+
+class RequestLoggingFilter(
+ requestLoggingConfig: RequestLoggingConfig
+) : AbstractRequestLoggingFilter() {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(RequestLoggingFilter::class.java)
+ }
+
+ init {
+ isIncludeClientInfo = requestLoggingConfig.includeClientInfo
+ isIncludeHeaders = requestLoggingConfig.includeHeaders
+ isIncludeQueryString = requestLoggingConfig.includeQueryString
+ isIncludePayload = requestLoggingConfig.includePayload
+ maxPayloadLength = requestLoggingConfig.maxPayloadLength
+ setAfterMessagePrefix("")
+ setAfterMessageSuffix("")
+ }
+
+ override fun beforeRequest(request: HttpServletRequest, message: String) {}
+
+ override fun afterRequest(request: HttpServletRequest, message: String) {
+ log.info(message)
+ }
+}
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java b/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java
deleted file mode 100644
index 4fcb95e5a..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package lavalink.server.io;
-
-import org.jetbrains.annotations.NotNull;
-import org.springframework.stereotype.Component;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-
-@Component
-public class ResponseHeaderFilter extends OncePerRequestFilter {
-
- @Override
- protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
- @NotNull FilterChain filterChain) throws IOException, ServletException {
- response.addHeader("Lavalink-Api-Version", "3");
- filterChain.doFilter(request, response);
- }
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.kt b/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.kt
new file mode 100644
index 000000000..d6cae7131
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.kt
@@ -0,0 +1,19 @@
+package lavalink.server.io
+
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+import javax.servlet.FilterChain
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+@Component
+class ResponseHeaderFilter : OncePerRequestFilter() {
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ response.addHeader("Lavalink-Api-Version", "3")
+ filterChain.doFilter(request, response)
+ }
+}
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/RoutePlannerRestHandler.kt b/LavalinkServer/src/main/java/lavalink/server/io/RoutePlannerRestHandler.kt
index aa3791082..e45362ff3 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/RoutePlannerRestHandler.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/RoutePlannerRestHandler.kt
@@ -4,7 +4,7 @@ import com.sedmelluq.lava.extensions.youtuberotator.planner.AbstractRoutePlanner
import com.sedmelluq.lava.extensions.youtuberotator.planner.NanoIpRoutePlanner
import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingIpRoutePlanner
import com.sedmelluq.lava.extensions.youtuberotator.planner.RotatingNanoIpRoutePlanner
-import org.json.JSONObject
+import dev.arbjerg.lavalink.protocol.v3.*
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
@@ -23,13 +23,13 @@ class RoutePlannerRestHandler(private val routePlanner: AbstractRoutePlanner?) {
/**
* Returns current information about the active AbstractRoutePlanner
*/
- @GetMapping("/routeplanner/status")
+ @GetMapping(value = ["/routeplanner/status", "/v3/routeplanner/status"])
fun getStatus(request: HttpServletRequest): ResponseEntity {
val status = when (routePlanner) {
null -> RoutePlannerStatus(null, null)
else -> RoutePlannerStatus(
- routePlanner.javaClass.simpleName,
- getDetailBlock(routePlanner)
+ routePlanner.javaClass.simpleName,
+ getDetailBlock(routePlanner)
)
}
return ResponseEntity.ok(status)
@@ -38,12 +38,14 @@ class RoutePlannerRestHandler(private val routePlanner: AbstractRoutePlanner?) {
/**
* Removes a single address from the addresses which are currently marked as failing
*/
- @PostMapping("/routeplanner/free/address")
- fun freeSingleAddress(request: HttpServletRequest, @RequestBody requestBody: String): ResponseEntity {
+ @PostMapping(value = ["/routeplanner/free/address", "/v3/routeplanner/free/address"])
+ fun freeSingleAddress(
+ request: HttpServletRequest,
+ @RequestBody body: RoutePlannerFreeAddress
+ ): ResponseEntity {
routePlanner ?: throw RoutePlannerDisabledException()
try {
- val jsonObject = JSONObject(requestBody)
- val address = InetAddress.getByName(jsonObject.getString("address"))
+ val address = InetAddress.getByName(body.address)
routePlanner.freeAddress(address)
return ResponseEntity.noContent().build()
} catch (exception: UnknownHostException) {
@@ -54,8 +56,8 @@ class RoutePlannerRestHandler(private val routePlanner: AbstractRoutePlanner?) {
/**
* Removes all addresses from the list which holds the addresses which are marked failing
*/
- @PostMapping("/routeplanner/free/all")
- fun freeAllAddresses(request: HttpServletRequest): ResponseEntity {
+ @PostMapping(value = ["/routeplanner/free/all", "/v3/routeplanner/free/all"])
+ fun freeAllAddresses(request: HttpServletRequest): ResponseEntity {
routePlanner ?: throw RoutePlannerDisabledException()
routePlanner.freeAllAddresses()
return ResponseEntity.noContent().build()
@@ -75,58 +77,31 @@ class RoutePlannerRestHandler(private val routePlanner: AbstractRoutePlanner?) {
return when (planner) {
is RotatingIpRoutePlanner -> RotatingIpRoutePlannerStatus(
- ipBlockStatus,
- failingAddressesStatus,
- planner.rotateIndex.toString(),
- planner.index.toString(),
- planner.currentAddress.toString()
+ ipBlockStatus,
+ failingAddressesStatus,
+ planner.rotateIndex.toString(),
+ planner.index.toString(),
+ planner.currentAddress.toString()
)
+
is NanoIpRoutePlanner -> NanoIpRoutePlannerStatus(
- ipBlockStatus,
- failingAddressesStatus,
- planner.currentAddress.toString()
+ ipBlockStatus,
+ failingAddressesStatus,
+ planner.currentAddress.toString()
)
+
is RotatingNanoIpRoutePlanner -> RotatingNanoIpRoutePlannerStatus(
- ipBlockStatus,
- failingAddressesStatus,
- planner.currentBlock.toString(),
- planner.addressIndexInBlock.toString()
+ ipBlockStatus,
+ failingAddressesStatus,
+ planner.currentBlock.toString(),
+ planner.addressIndexInBlock.toString()
)
+
else -> GenericRoutePlannerStatus(ipBlockStatus, failingAddressesStatus)
}
}
- data class RoutePlannerStatus(val `class`: String?, val details: IRoutePlannerStatus?)
-
- interface IRoutePlannerStatus
- data class GenericRoutePlannerStatus(
- val ipBlock: IpBlockStatus,
- val failingAddresses: List
- ) : IRoutePlannerStatus
-
- data class RotatingIpRoutePlannerStatus(
- val ipBlock: IpBlockStatus,
- val failingAddresses: List,
- val rotateIndex: String,
- val ipIndex: String,
- val currentAddress: String
- ) : IRoutePlannerStatus
-
- data class NanoIpRoutePlannerStatus(
- val ipBlock: IpBlockStatus,
- val failingAddresses: List,
- val currentAddressIndex: String
- ) : IRoutePlannerStatus
-
- data class RotatingNanoIpRoutePlannerStatus(
- val ipBlock: IpBlockStatus,
- val failingAddresses: List,
- val blockIndex: String,
- val currentAddressIndex: String
- ) : IRoutePlannerStatus
-
- data class FailingAddress(val failingAddress: String, val failingTimestamp: Long, val failingTime: String)
- data class IpBlockStatus(val type: String, val size: String)
+ class RoutePlannerDisabledException :
+ ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Can't access disabled route planner")
- class RoutePlannerDisabledException : ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Can't access disabled route planner")
}
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SessionRestHandler.kt b/LavalinkServer/src/main/java/lavalink/server/io/SessionRestHandler.kt
new file mode 100644
index 000000000..10a31a459
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/io/SessionRestHandler.kt
@@ -0,0 +1,34 @@
+package lavalink.server.io
+
+import dev.arbjerg.lavalink.protocol.v3.Session
+import dev.arbjerg.lavalink.protocol.v3.SessionUpdate
+import dev.arbjerg.lavalink.protocol.v3.takeIfPresent
+import lavalink.server.util.socketContext
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.PatchMapping
+import org.springframework.web.bind.annotation.PathVariable
+import org.springframework.web.bind.annotation.RequestBody
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+class SessionRestHandler(private val socketServer: SocketServer) {
+
+ @PatchMapping("/v3/sessions/{sessionId}")
+ private fun patchSession(
+ @RequestBody sessionUpdate: SessionUpdate,
+ @PathVariable sessionId: String
+ ): ResponseEntity {
+ val context = socketContext(socketServer, sessionId)
+
+ sessionUpdate.resumingKey.takeIfPresent {
+ context.resumeKey = it
+ }
+
+ sessionUpdate.timeout.takeIfPresent {
+ context.resumeTimeout = it
+ }
+
+ return ResponseEntity.ok(Session(context.resumeKey, context.resumeTimeout))
+ }
+
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt
index 28adbef9a..21281f7eb 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt
@@ -22,17 +22,19 @@
package lavalink.server.io
+import com.fasterxml.jackson.databind.ObjectMapper
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager
import dev.arbjerg.lavalink.api.AudioFilterExtension
import dev.arbjerg.lavalink.api.ISocketContext
import dev.arbjerg.lavalink.api.PluginEventHandler
import dev.arbjerg.lavalink.api.WebSocketExtension
+import dev.arbjerg.lavalink.protocol.v3.Message
import io.undertow.websockets.core.WebSocketCallback
import io.undertow.websockets.core.WebSocketChannel
import io.undertow.websockets.core.WebSockets
import io.undertow.websockets.jsr.UndertowSession
import lavalink.server.config.ServerConfig
-import lavalink.server.player.Player
+import lavalink.server.player.LavalinkPlayer
import moe.kyokobot.koe.KoeClient
import moe.kyokobot.koe.KoeEventAdapter
import moe.kyokobot.koe.MediaConnection
@@ -43,36 +45,33 @@ import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.adapter.standard.StandardWebSocketSession
import java.net.InetSocketAddress
import java.util.*
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.ConcurrentLinkedQueue
-import java.util.concurrent.Executors
-import java.util.concurrent.ScheduledExecutorService
-import java.util.concurrent.ScheduledFuture
-import java.util.concurrent.TimeUnit
+import java.util.concurrent.*
class SocketContext(
+ private val sessionId: String,
val audioPlayerManager: AudioPlayerManager,
- val serverConfig: ServerConfig,
+ private val serverConfig: ServerConfig,
private var session: WebSocketSession,
private val socketServer: SocketServer,
+ statsCollector: StatsCollector,
private val userId: String,
private val clientName: String?,
val koe: KoeClient,
eventHandlers: Collection,
webSocketExtensions: List,
- filterExtensions: List
-
+ filterExtensions: List,
+ private val objectMapper: ObjectMapper
) : ISocketContext {
companion object {
private val log = LoggerFactory.getLogger(SocketContext::class.java)
}
- //guildId <-> Player
- private val players = ConcurrentHashMap()
+ //guildId <-> LavalinkPlayer
+ private val players = ConcurrentHashMap()
val eventEmitter = EventEmitter(this, eventHandlers)
- val wsHandler = WebSocketHandler(this, webSocketExtensions, filterExtensions, serverConfig.filters)
+ val wsHandler = WebSocketHandler(this, webSocketExtensions, filterExtensions, serverConfig, objectMapper)
@Volatile
var sessionPaused = false
@@ -85,16 +84,16 @@ class SocketContext(
private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
val playerUpdateService: ScheduledExecutorService
- val playingPlayers: List
+ val playingPlayers: List
get() {
- val newList = LinkedList()
+ val newList = LinkedList()
players.values.forEach { player -> if (player.isPlaying) newList.add(player) }
return newList
}
init {
- executor.scheduleAtFixedRate(StatsTask(this, socketServer), 0, 1, TimeUnit.MINUTES)
+ executor.scheduleAtFixedRate(statsCollector.createTask(this), 0, 1, TimeUnit.MINUTES)
playerUpdateService = Executors.newScheduledThreadPool(2) { r ->
val thread = Thread(r)
@@ -104,7 +103,9 @@ class SocketContext(
}
}
- fun getPlayer(guildId: String) = getPlayer(guildId.toLong())
+ override fun getSessionId(): String {
+ return sessionId
+ }
override fun getUserId(): Long {
return userId.toLong()
@@ -115,19 +116,19 @@ class SocketContext(
}
override fun getPlayer(guildId: Long) = players.computeIfAbsent(guildId) {
- val player = Player(this, guildId, audioPlayerManager, serverConfig)
+ val player = LavalinkPlayer(this, guildId, serverConfig, audioPlayerManager)
eventEmitter.onNewPlayer(player)
player
}
- override fun getPlayers(): Map {
+ override fun getPlayers(): Map {
return players.toMap()
}
/**
* Gets or creates a media connection
*/
- fun getMediaConnection(player: Player): MediaConnection {
+ fun getMediaConnection(player: LavalinkPlayer): MediaConnection {
val guildId = player.guildId
var conn = koe.getConnection(guildId)
if (conn == null) {
@@ -158,7 +159,11 @@ class SocketContext(
}
override fun sendMessage(message: JSONObject) {
- send(message)
+ send(message.toString())
+ }
+
+ override fun sendMessage(message: Any) {
+ send(objectMapper.writeValueAsString(message))
}
override fun getState(): ISocketContext.State = when {
@@ -170,8 +175,6 @@ class SocketContext(
/**
* Either sends the payload now or queues it up
*/
- fun send(payload: JSONObject) = send(payload.toString())
-
private fun send(payload: String) {
eventEmitter.onWebSocketMessageOut(payload)
@@ -186,7 +189,7 @@ class SocketContext(
WebSockets.sendText(payload, undertowSession.webSocketChannel,
object : WebSocketCallback {
override fun complete(channel: WebSocketChannel, context: Void?) {
- log.trace("Sent {}", payload)
+ log.trace("Sent $payload")
}
override fun onError(channel: WebSocketChannel, context: Void?, throwable: Throwable) {
@@ -214,7 +217,7 @@ class SocketContext(
}
internal fun shutdown() {
- log.info("Shutting down " + playingPlayers.size + " playing players.")
+ log.info("Shutting down ${playingPlayers.size} playing players.")
executor.shutdown()
playerUpdateService.shutdown()
players.values.forEach {
@@ -236,18 +239,10 @@ class SocketContext(
session.close()
}
- private inner class WsEventHandler(private val player: Player) : KoeEventAdapter() {
+ private inner class WsEventHandler(private val player: LavalinkPlayer) : KoeEventAdapter() {
override fun gatewayClosed(code: Int, reason: String?, byRemote: Boolean) {
- val out = JSONObject()
- out.put("op", "event")
- out.put("type", "WebSocketClosedEvent")
- out.put("guildId", player.guildId.toString())
- out.put("reason", reason ?: "")
- out.put("code", code)
- out.put("byRemote", byRemote)
-
- send(out)
-
+ val event = Message.WebSocketClosedEvent(code, reason ?: "", byRemote, player.guildId.toString())
+ sendMessage(event)
SocketServer.sendPlayerUpdate(this@SocketContext, player)
}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt
index 1f7a51ddc..c3225bcfa 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt
@@ -22,12 +22,15 @@
package lavalink.server.io
+import com.fasterxml.jackson.databind.ObjectMapper
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager
import dev.arbjerg.lavalink.api.AudioFilterExtension
import dev.arbjerg.lavalink.api.PluginEventHandler
import dev.arbjerg.lavalink.api.WebSocketExtension
+import dev.arbjerg.lavalink.protocol.v3.Message
+import dev.arbjerg.lavalink.protocol.v3.PlayerState
import lavalink.server.config.ServerConfig
-import lavalink.server.player.Player
+import lavalink.server.player.LavalinkPlayer
import moe.kyokobot.koe.Koe
import moe.kyokobot.koe.KoeOptions
import org.json.JSONObject
@@ -41,37 +44,51 @@ import java.util.concurrent.ConcurrentHashMap
@Service
class SocketServer(
- private val serverConfig: ServerConfig,
- private val audioPlayerManager: AudioPlayerManager,
- koeOptions: KoeOptions,
- private val eventHandlers: List,
- private val webSocketExtensions: List,
- private val filterExtensions: List
+ private val serverConfig: ServerConfig,
+ val audioPlayerManager: AudioPlayerManager,
+ koeOptions: KoeOptions,
+ private val eventHandlers: List,
+ private val webSocketExtensions: List,
+ private val filterExtensions: List,
+ private val objectMapper: ObjectMapper
) : TextWebSocketHandler() {
- // userId <-> shardCount
+ // sessionID <-> Session
val contextMap = ConcurrentHashMap()
private val resumableSessions = mutableMapOf()
private val koe = Koe.koe(koeOptions)
+ private val statsCollector = StatsCollector(this)
+ private val charPool = ('a'..'z') + ('0'..'9')
companion object {
private val log = LoggerFactory.getLogger(SocketServer::class.java)
- fun sendPlayerUpdate(socketContext: SocketContext, player: Player) {
- val json = JSONObject()
+ fun sendPlayerUpdate(socketContext: SocketContext, player: LavalinkPlayer) {
+ if (socketContext.sessionPaused) return
- val state = player.state
val connection = socketContext.getMediaConnection(player).gatewayConnection
- state.put("connected", connection?.isOpen == true)
- state.put("ping", connection?.ping ?: -1)
-
- json.put("op", "playerUpdate")
- json.put("guildId", player.guildId.toString())
- json.put("state", state)
- socketContext.send(json)
+ socketContext.sendMessage(
+ Message.PlayerUpdateEvent(
+ PlayerState(
+ System.currentTimeMillis(),
+ player.audioPlayer.playingTrack?.position ?: 0,
+ connection?.isOpen == true,
+ connection?.ping ?: -1L
+ ),
+ player.guildId.toString()
+ )
+ )
}
}
+ private fun generateUniqueSessionId(): String {
+ var sessionId: String
+ do {
+ sessionId = List(16) { charPool.random() }.joinToString("")
+ } while (contextMap[sessionId] != null)
+ return sessionId
+ }
+
val contexts: Collection
get() = contextMap.values
@@ -86,27 +103,35 @@ class SocketServer(
if (resumeKey != null) resumable = resumableSessions.remove(resumeKey)
if (resumable != null) {
- contextMap[session.id] = resumable
+ contextMap[resumable.sessionId] = resumable
resumable.resume(session)
log.info("Resumed session with key $resumeKey")
resumable.eventEmitter.onWebSocketOpen(true)
+ resumable.sendMessage(Message.ReadyEvent(true, resumable.sessionId))
return
}
+ val sessionId = generateUniqueSessionId()
+ session.attributes["sessionId"] = sessionId
+
val socketContext = SocketContext(
- audioPlayerManager,
- serverConfig,
- session,
- this,
- userId,
- clientName,
- koe.newClient(userId.toLong()),
- eventHandlers,
- webSocketExtensions,
- filterExtensions
+ sessionId,
+ audioPlayerManager,
+ serverConfig,
+ session,
+ this,
+ statsCollector,
+ userId,
+ clientName,
+ koe.newClient(userId.toLong()),
+ eventHandlers,
+ webSocketExtensions,
+ filterExtensions,
+ objectMapper
)
- contextMap[session.id] = socketContext
+ contextMap[sessionId] = socketContext
socketContext.eventEmitter.onWebSocketOpen(false)
+ socketContext.sendMessage(Message.ReadyEvent(false, sessionId))
if (clientName != null) {
log.info("Connection successfully established from $clientName")
@@ -121,52 +146,42 @@ class SocketServer(
}
}
- override fun afterConnectionClosed(session: WebSocketSession?, status: CloseStatus?) {
- val context = contextMap.remove(session!!.id) ?: return
+ override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
+ val context = contextMap.remove(session.id) ?: return
if (context.resumeKey != null) {
resumableSessions.remove(context.resumeKey!!)?.let { removed ->
- log.warn("Shutdown resumable session with key ${removed.resumeKey} because it has the same key as a " +
- "newly disconnected resumable session.")
+ log.warn(
+ "Shutdown resumable session with key ${removed.resumeKey} because it has the same key as a " +
+ "newly disconnected resumable session."
+ )
removed.shutdown()
}
resumableSessions[context.resumeKey!!] = context
context.pause()
- log.info("Connection closed from {} with status {} -- " +
- "Session can be resumed within the next {} seconds with key {}",
- session.remoteAddress,
- status,
- context.resumeTimeout,
- context.resumeKey
+ log.info(
+ "Connection closed from ${session.remoteAddress} with status $status -- " +
+ "Session can be resumed within the next ${context.resumeTimeout} seconds with key ${context.resumeKey}",
)
return
}
- log.info("Connection closed from {} -- {}", session.remoteAddress, status)
+ log.info("Connection closed from ${session.remoteAddress} -- $status")
context.shutdown()
}
- override fun handleTextMessage(session: WebSocketSession?, message: TextMessage?) {
- try {
- handleTextMessageSafe(session!!, message!!)
- } catch (e: Exception) {
- log.error("Exception while handling websocket message", e)
- }
-
- }
-
- private fun handleTextMessageSafe(session: WebSocketSession, message: TextMessage) {
+ override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val json = JSONObject(message.payload)
log.info(message.payload)
if (!session.isOpen) {
- log.error("Ignoring closing websocket: " + session.remoteAddress!!)
+ log.error("Ignoring closing websocket: ${session.remoteAddress!!}")
return
}
- val context = contextMap[session.id]
- ?: throw IllegalStateException("No context for session ID ${session.id}. Broken websocket?")
+ val context = contextMap[session.attributes["sessionId"]]
+ ?: throw IllegalStateException("No context for session ID ${session.id}. Broken websocket?")
context.eventEmitter.onWebsocketMessageIn(message.payload)
context.wsHandler.handle(json)
}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsCollector.kt b/LavalinkServer/src/main/java/lavalink/server/io/StatsCollector.kt
new file mode 100644
index 000000000..d70f1fc56
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/io/StatsCollector.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2021 Freya Arbjerg and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package lavalink.server.io
+
+import dev.arbjerg.lavalink.protocol.v3.*
+import lavalink.server.Launcher
+import lavalink.server.player.AudioLossCounter
+import org.slf4j.LoggerFactory
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+import oshi.SystemInfo
+import kotlin.Exception
+
+@RestController
+class StatsCollector(val socketServer: SocketServer) {
+ companion object {
+ private val log = LoggerFactory.getLogger(StatsCollector::class.java)
+
+ private val si = SystemInfo()
+ private val hal get() = si.hardware
+ private val os get() = si.operatingSystem
+
+ private var prevTicks: LongArray? = null
+ }
+
+ private var uptime = 0.0
+ private var cpuTime = 0.0
+
+ // Record for next invocation
+ private val processRecentCpuUsage: Double
+ get() {
+ val p = os.getProcess(os.processId)
+
+ val output: Double = if (cpuTime != 0.0) {
+ val uptimeDiff = p.upTime - uptime
+ val cpuDiff = p.kernelTime + p.userTime - cpuTime
+ cpuDiff / uptimeDiff
+ } else {
+ (p.kernelTime + p.userTime).toDouble() / p.userTime.toDouble()
+ }
+
+ // Record for next invocation
+ uptime = p.upTime.toDouble()
+ cpuTime = (p.kernelTime + p.userTime).toDouble()
+ return output / hal.processor.logicalProcessorCount
+ }
+
+ fun createTask(context: SocketContext): Runnable = Runnable {
+ try {
+ val stats = retrieveStats(context)
+ context.sendMessage(Message.StatsEvent(stats))
+ } catch (e: Exception) {
+ log.error("Exception while sending stats", e)
+ }
+ }
+
+ @GetMapping("/v3/stats")
+ fun getStats() = retrieveStats()
+
+ fun retrieveStats(context: SocketContext? = null): Stats {
+ val playersTotal = intArrayOf(0)
+ val playersPlaying = intArrayOf(0)
+ socketServer.contexts.forEach { socketContext ->
+ playersTotal[0] += socketContext.players.size
+ playersPlaying[0] += socketContext.playingPlayers.size
+ }
+
+ val uptime = System.currentTimeMillis() - Launcher.startTime
+
+ // In bytes
+ val runtime = Runtime.getRuntime()
+ val mem = Memory(
+ free = runtime.freeMemory(),
+ used = runtime.totalMemory() - runtime.freeMemory(),
+ allocated = runtime.totalMemory(),
+ reservable = runtime.maxMemory()
+ )
+
+ // prevTicks will be null so set it to a value.
+ if (prevTicks == null) {
+ prevTicks = hal.processor.systemCpuLoadTicks
+ }
+
+ val cpu = Cpu(
+ runtime.availableProcessors(),
+ systemLoad = hal.processor.getSystemCpuLoadBetweenTicks(prevTicks),
+ lavalinkLoad = processRecentCpuUsage.takeIf { it.isFinite() } ?: 0.0
+ )
+
+ // Set new prevTicks to current value for more accurate baseline, and checks in the next schedule.
+ prevTicks = hal.processor.systemCpuLoadTicks
+
+ var frameStats: FrameStats? = null
+ if (context != null) {
+ var playerCount = 0
+ var totalSent = 0
+ var totalNulled = 0
+ for (player in context.playingPlayers) {
+ val counter = player.audioLossCounter
+ if (!counter.isDataUsable) continue
+ playerCount++
+ totalSent += counter.lastMinuteSuccess
+ totalNulled += counter.lastMinuteLoss
+ }
+
+ // We can't divide by 0
+ if (playerCount != 0) {
+ val totalDeficit = playerCount *
+ AudioLossCounter.EXPECTED_PACKET_COUNT_PER_MIN -
+ (totalSent + totalNulled)
+
+ frameStats = FrameStats(
+ (totalSent / playerCount),
+ (totalNulled / playerCount),
+ (totalDeficit / playerCount)
+ )
+ }
+ }
+
+ return Stats(
+ frameStats,
+ playersTotal[0],
+ playersPlaying[0],
+ uptime,
+ mem,
+ cpu
+ )
+ }
+}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java
deleted file mode 100644
index 0a75215e8..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (c) 2021 Freya Arbjerg and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package lavalink.server.io;
-
-import lavalink.server.Launcher;
-import lavalink.server.player.AudioLossCounter;
-import lavalink.server.player.Player;
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import oshi.SystemInfo;
-import oshi.hardware.HardwareAbstractionLayer;
-import oshi.software.os.OSProcess;
-import oshi.software.os.OperatingSystem;
-
-public class StatsTask implements Runnable {
-
- private static final Logger log = LoggerFactory.getLogger(StatsTask.class);
-
- private final SocketContext context;
- private final SocketServer socketServer;
-
- private final SystemInfo si = new SystemInfo();
- private final HardwareAbstractionLayer hal = si.getHardware();
- /** CPU ticks used for calculations in CPU load. */
- private long[] prevTicks;
-
- StatsTask(SocketContext context, SocketServer socketServer) {
- this.context = context;
- this.socketServer = socketServer;
- }
-
- @Override
- public void run() {
- try {
- sendStats();
- } catch (Exception e) {
- log.error("Exception while sending stats", e);
- }
- }
-
- private void sendStats() {
- if (context.getSessionPaused()) return;
-
- JSONObject out = new JSONObject();
-
- final int[] playersTotal = {0};
- final int[] playersPlaying = {0};
-
- socketServer.getContexts().forEach(socketContext -> {
- playersTotal[0] += socketContext.getPlayers().size();
- playersPlaying[0] += socketContext.getPlayingPlayers().size();
- });
-
- out.put("op", "stats");
- out.put("players", playersTotal[0]);
- out.put("playingPlayers", playersPlaying[0]);
- out.put("uptime", System.currentTimeMillis() - Launcher.INSTANCE.getStartTime());
-
- // In bytes
- JSONObject mem = new JSONObject();
- mem.put("free", Runtime.getRuntime().freeMemory());
- mem.put("used", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
- mem.put("allocated", Runtime.getRuntime().totalMemory());
- mem.put("reservable", Runtime.getRuntime().maxMemory());
- out.put("memory", mem);
-
-
- JSONObject cpu = new JSONObject();
- cpu.put("cores", Runtime.getRuntime().availableProcessors());
- // prevTicks will be null so set it to a value.
- if(prevTicks == null) {
- prevTicks = hal.getProcessor().getSystemCpuLoadTicks();
- }
- // Compare current CPU ticks with previous to establish a CPU load and return double.
- cpu.put("systemLoad", hal.getProcessor().getSystemCpuLoadBetweenTicks(prevTicks));
- // Set new prevTicks to current value for more accurate baseline, and checks in next schedule.
- prevTicks = hal.getProcessor().getSystemCpuLoadTicks();
- double load = getProcessRecentCpuUsage();
- if (!Double.isFinite(load)) load = 0;
- cpu.put("lavalinkLoad", load);
-
- out.put("cpu", cpu);
-
- int totalSent = 0;
- int totalNulled = 0;
- int players = 0;
-
- for (Player player : context.getPlayingPlayers()) {
- AudioLossCounter counter = player.getAudioLossCounter();
- if (!counter.isDataUsable()) continue;
-
- players++;
- totalSent += counter.getLastMinuteSuccess();
- totalNulled += counter.getLastMinuteLoss();
- }
-
- int totalDeficit = players * AudioLossCounter.EXPECTED_PACKET_COUNT_PER_MIN
- - (totalSent + totalNulled);
-
- // We can't divide by 0
- if (players != 0) {
- JSONObject frames = new JSONObject();
- frames.put("sent", totalSent / players);
- frames.put("nulled", totalNulled / players);
- frames.put("deficit", totalDeficit / players);
- out.put("frameStats", frames);
- }
-
- context.send(out);
- }
-
- private double uptime = 0;
- private double cpuTime = 0;
-
- private double getProcessRecentCpuUsage() {
- double output;
- HardwareAbstractionLayer hal = si.getHardware();
- OperatingSystem os = si.getOperatingSystem();
- OSProcess p = os.getProcess(os.getProcessId());
-
- if (cpuTime != 0) {
- double uptimeDiff = p.getUpTime() - uptime;
- double cpuDiff = (p.getKernelTime() + p.getUserTime()) - cpuTime;
- output = cpuDiff / uptimeDiff;
- } else {
- output = ((double) (p.getKernelTime() + p.getUserTime())) / (double) p.getUserTime();
- }
-
- // Record for next invocation
- uptime = p.getUpTime();
- cpuTime = p.getKernelTime() + p.getUserTime();
- return output / hal.getProcessor().getLogicalProcessorCount();
- }
-
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/WSCodes.java b/LavalinkServer/src/main/java/lavalink/server/io/WSCodes.java
deleted file mode 100644
index a7cf5e18c..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/io/WSCodes.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (c) 2021 Freya Arbjerg and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package lavalink.server.io;
-
-public class WSCodes {
-
- public static final int INTERNAL_ERROR = 4000;
- public static final int AUTHORIZATION_REJECTED = 4001;
-
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandler.kt b/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandler.kt
index 933f61467..e79db30ca 100644
--- a/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandler.kt
+++ b/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandler.kt
@@ -1,30 +1,38 @@
package lavalink.server.io
+import com.fasterxml.jackson.databind.ObjectMapper
import com.sedmelluq.discord.lavaplayer.track.TrackMarker
import dev.arbjerg.lavalink.api.AudioFilterExtension
import dev.arbjerg.lavalink.api.WebSocketExtension
+import dev.arbjerg.lavalink.protocol.v3.Filters
+import dev.arbjerg.lavalink.protocol.v3.decodeTrack
+import lavalink.server.config.ServerConfig
import lavalink.server.player.TrackEndMarkerHandler
import lavalink.server.player.filters.Band
+import lavalink.server.player.filters.EqualizerConfig
import lavalink.server.player.filters.FilterChain
-import lavalink.server.util.Util
import moe.kyokobot.koe.VoiceServerInfo
import org.json.JSONObject
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import kotlin.reflect.KFunction1
class WebSocketHandler(
private val context: SocketContext,
- private val wsExtensions: List,
+ wsExtensions: List,
private val filterExtensions: List,
- private val filterConfig: Map
+ serverConfig: ServerConfig,
+ private val objectMapper: ObjectMapper
) {
-
companion object {
private val log: Logger = LoggerFactory.getLogger(WebSocketHandler::class.java)
+
+ fun WebSocketExtension.toHandler(ctx: SocketContext): Pair Unit> {
+ return opName to { onInvocation(ctx, it) }
+ }
}
private var loggedEqualizerDeprecationWarning = false
+ private var loggedWsCommandsDeprecationWarning = false
private val handlers: Map Unit> = mutableMapOf(
"voiceUpdate" to ::voiceUpdate,
@@ -37,14 +45,15 @@ class WebSocketHandler(
"filters" to ::filters,
"destroy" to ::destroy,
"configureResuming" to ::configureResuming
- ).apply {
- wsExtensions.forEach {
- val func = fun(json: JSONObject) { it.onInvocation(context, json) }
- this[it.opName] = func as KFunction1
- }
- }
+ ) + wsExtensions.associate { it.toHandler(context) }
+
+ private val disabledFilters = serverConfig.filters.entries.filter { !it.value }.map { it.key }
fun handle(json: JSONObject) {
+ if (!loggedWsCommandsDeprecationWarning) {
+ log.warn("Sending websocket commands to Lavalink has been deprecated and will be removed in API version 4. API version 3 will be removed in Lavalink 5. Please use the new REST endpoints instead.")
+ loggedWsCommandsDeprecationWarning = true
+ }
val op = json.getString("op")
val handler = handlers[op] ?: return log.warn("Unknown op '$op'")
handler(json)
@@ -71,15 +80,15 @@ class WebSocketHandler(
}
private fun play(json: JSONObject) {
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
val noReplace = json.optBoolean("noReplace", false)
- if (noReplace && player.playingTrack != null) {
+ if (noReplace && player.track != null) {
log.info("Skipping play request because of noReplace")
return
}
- val track = Util.toAudioTrack(context.audioPlayerManager, json.getString("track"))
+ val track = decodeTrack(context.audioPlayerManager, json.getString("track"))
if (json.has("startTime")) {
track.position = json.getLong("startTime")
@@ -102,53 +111,62 @@ class WebSocketHandler(
player.play(track)
val conn = context.getMediaConnection(player)
- context.getPlayer(json.getString("guildId")).provideTo(conn)
+ context.getPlayer(json.getLong("guildId")).provideTo(conn)
}
private fun stop(json: JSONObject) {
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
player.stop()
}
private fun pause(json: JSONObject) {
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
player.setPause(json.getBoolean("pause"))
SocketServer.sendPlayerUpdate(context, player)
}
private fun seek(json: JSONObject) {
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
player.seekTo(json.getLong("position"))
SocketServer.sendPlayerUpdate(context, player)
}
private fun volume(json: JSONObject) {
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
player.setVolume(json.getInt("volume"))
}
private fun equalizer(json: JSONObject) {
- if (!loggedEqualizerDeprecationWarning) log.warn("The 'equalizer' op has been deprecated in favour of the " +
- "'filters' op. Please switch to use that one, as this op will get removed in v4.")
- loggedEqualizerDeprecationWarning = true
+ if (!loggedEqualizerDeprecationWarning) {
+ log.warn(
+ "The 'equalizer' op has been deprecated in favour of the " +
+ "'filters' op. Please switch to that one, as this op will be removed in API version 4."
+ )
- if (filterConfig["equalizer"] == false) return log.warn("Equalizer is disabled in the config, ignoring equalizer op")
+ loggedEqualizerDeprecationWarning = true
+ }
+ if ("equalizer" in disabledFilters) return log.warn("Equalizer filter is disabled in the config, ignoring equalizer op")
- val player = context.getPlayer(json.getString("guildId"))
+ val player = context.getPlayer(json.getLong("guildId"))
- val list = mutableListOf()
- json.getJSONArray("bands").forEach { b ->
- val band = b as JSONObject
- list.add(Band(band.getInt("band"), band.getFloat("gain")))
- }
- val filters = player.filters ?: FilterChain()
- filters.equalizer = list
+ val bands = json.getJSONArray("bands")
+ .filterIsInstance()
+ .map { b -> Band(b.getInt("band"), b.getFloat("gain")) }
+
+ val filters = player.filters
+ filters.setEqualizer(EqualizerConfig(bands))
player.filters = filters
}
private fun filters(json: JSONObject) {
val player = context.getPlayer(json.getLong("guildId"))
- player.filters = FilterChain.parse(json, filterExtensions, filterConfig)
+ val filters = objectMapper.readValue(json.toString(), Filters::class.java)
+ val invalidFilters = filters.validate(disabledFilters)
+ if (invalidFilters.isNotEmpty()) {
+ log.warn("The following filters are disabled in the config and are being ignored: $invalidFilters")
+ return
+ }
+ player.filters = FilterChain.parse(filters, filterExtensions)
}
private fun destroy(json: JSONObject) {
diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java
deleted file mode 100644
index a32276633..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (c) 2021 Freya Arbjerg and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-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;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class AudioLoader implements AudioLoadResultHandler {
-
- private static final Logger log = LoggerFactory.getLogger(AudioLoader.class);
- private static final LoadResult NO_MATCHES = new LoadResult(ResultStatus.NO_MATCHES, Collections.emptyList(),
- null, null);
-
- private final AudioPlayerManager audioPlayerManager;
-
- private final CompletableFuture loadResult = new CompletableFuture<>();
- private final AtomicBoolean used = new AtomicBoolean(false);
-
- public AudioLoader(AudioPlayerManager audioPlayerManager) {
- this.audioPlayerManager = audioPlayerManager;
- }
-
- public CompletionStage load(String identifier) {
- boolean isUsed = this.used.getAndSet(true);
- if (isUsed) {
- throw new IllegalStateException("This loader can only be used once per instance");
- }
-
- log.trace("Loading item with identifier {}", identifier);
- this.audioPlayerManager.loadItem(identifier, this);
-
- return loadResult;
- }
-
- @Override
- public void trackLoaded(AudioTrack audioTrack) {
- log.info("Loaded track " + audioTrack.getInfo().title);
- ArrayList result = new ArrayList<>();
- result.add(audioTrack);
- this.loadResult.complete(new LoadResult(ResultStatus.TRACK_LOADED, result, null, null));
- }
-
- @Override
- public void playlistLoaded(AudioPlaylist audioPlaylist) {
- log.info("Loaded playlist " + audioPlaylist.getName());
-
- String playlistName = null;
- Integer selectedTrack = null;
- if (!audioPlaylist.isSearchResult()) {
- playlistName = audioPlaylist.getName();
- selectedTrack = audioPlaylist.getTracks().indexOf(audioPlaylist.getSelectedTrack());
- }
-
- ResultStatus status = audioPlaylist.isSearchResult() ? ResultStatus.SEARCH_RESULT : ResultStatus.PLAYLIST_LOADED;
- List loadedItems = audioPlaylist.getTracks();
-
- this.loadResult.complete(new LoadResult(status, loadedItems, playlistName, selectedTrack));
- }
-
- @Override
- public void noMatches() {
- log.info("No matches found");
- this.loadResult.complete(NO_MATCHES);
- }
-
- @Override
- public void loadFailed(FriendlyException e) {
- log.error("Load failed", e);
- this.loadResult.complete(new LoadResult(e));
- }
-
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.kt b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.kt
new file mode 100644
index 000000000..bb8bd976d
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2021 Freya Arbjerg and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+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
+import dev.arbjerg.lavalink.protocol.v3.LoadResult
+import lavalink.server.util.toPlaylistInfo
+import lavalink.server.util.toTrack
+import org.slf4j.LoggerFactory
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionStage
+import java.util.concurrent.atomic.AtomicBoolean
+
+class AudioLoader(private val audioPlayerManager: AudioPlayerManager) : AudioLoadResultHandler {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(AudioLoader::class.java)
+ }
+
+ private val loadResult = CompletableFuture()
+ private val used = AtomicBoolean(false)
+
+ fun load(identifier: String?): CompletionStage {
+ val isUsed = used.getAndSet(true)
+ check(!isUsed) { "This loader can only be used once per instance" }
+ log.trace("Loading item with identifier $identifier")
+ audioPlayerManager.loadItem(identifier, this)
+ return loadResult
+ }
+
+ override fun trackLoaded(audioTrack: AudioTrack) {
+ log.info("Loaded track ${audioTrack.info.title}")
+ val track = audioTrack.toTrack(audioPlayerManager)
+ loadResult.complete(LoadResult.trackLoaded(track))
+ }
+
+ override fun playlistLoaded(audioPlaylist: AudioPlaylist) {
+ log.info("Loaded playlist ${audioPlaylist.name}")
+ val tracks = audioPlaylist.tracks.map { it.toTrack(audioPlayerManager) }
+ if (audioPlaylist.isSearchResult) {
+ loadResult.complete(LoadResult.searchResultLoaded(tracks))
+ return
+ }
+ loadResult.complete(LoadResult.playlistLoaded(audioPlaylist.toPlaylistInfo(), tracks))
+ }
+
+ override fun noMatches() {
+ log.info("No matches found")
+ loadResult.complete(LoadResult.noMatches)
+ }
+
+ override fun loadFailed(e: FriendlyException) {
+ log.error("Load failed", e)
+ loadResult.complete(LoadResult.loadFailed(e))
+ }
+
+}
\ No newline at end of file
diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java
deleted file mode 100644
index 14aa32500..000000000
--- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (c) 2021 Freya Arbjerg and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-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.config.ServerConfig;
-import lavalink.server.util.Util;
-import org.json.JSONArray;
-import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.servlet.http.HttpServletRequest;
-import java.io.IOException;
-import java.util.concurrent.CompletionStage;
-
-@RestController
-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();
- log.info("GET " + path);
- }
-
- private JSONObject trackToJSON(AudioTrack audioTrack) {
- AudioTrackInfo trackInfo = audioTrack.getInfo();
-
- return new JSONObject()
- .put("title", trackInfo.title)
- .put("author", trackInfo.author)
- .put("length", trackInfo.length)
- .put("identifier", trackInfo.identifier)
- .put("uri", trackInfo.uri)
- .put("isStream", trackInfo.isStream)
- .put("isSeekable", audioTrack.isSeekable())
- .put("position", audioTrack.getPosition())
- .put("sourceName", audioTrack.getSourceManager() == null ? null : audioTrack.getSourceManager().getSourceName());
- }
-
- private JSONObject encodeLoadResult(LoadResult result) {
- JSONObject json = new JSONObject();
- JSONObject playlist = new JSONObject();
- JSONArray tracks = new JSONArray();
-
- result.tracks.forEach(track -> {
- JSONObject object = new JSONObject();
- object.put("info", trackToJSON(track));
-
- try {
- String encoded = Util.toMessage(audioPlayerManager, track);
- object.put("track", encoded);
- tracks.put(object);
- } catch (IOException e) {
- log.warn("Failed to encode a track {}, skipping", track.getIdentifier(), e);
- }
- });
-
- playlist.put("name", result.playlistName);
- playlist.put("selectedTrack", result.selectedTrack);
-
- json.put("playlistInfo", playlist);
- json.put("loadType", result.loadResultType);
- json.put("tracks", tracks);
-
- if (result.loadResultType == ResultStatus.LOAD_FAILED && result.exception != null) {
- JSONObject exception = new JSONObject();
- exception.put("message", result.exception.getLocalizedMessage());
- exception.put("severity", result.exception.severity.toString());
-
- json.put("exception", exception);
- log.error("Track loading failed", result.exception);
- }
-
- return json;
- }
-
- @GetMapping(value = "/loadtracks", produces = "application/json")
- @ResponseBody
- public CompletionStage> getLoadTracks(
- HttpServletRequest request,
- @RequestParam String identifier) {
- log.info("Got request to load for identifier \"{}\"", identifier);
-
- return new AudioLoader(audioPlayerManager).load(identifier)
- .thenApply(this::encodeLoadResult)
- .thenApply(loadResultJson -> new ResponseEntity<>(loadResultJson.toString(), HttpStatus.OK));
- }
-
- @GetMapping(value = "/decodetrack", produces = "application/json")
- @ResponseBody
- public ResponseEntity getDecodeTrack(HttpServletRequest request, @RequestParam String track)
- throws IOException {
-
- log(request);
-
- AudioTrack audioTrack = Util.toAudioTrack(audioPlayerManager, track);
-
- return new ResponseEntity<>(trackToJSON(audioTrack).toString(), HttpStatus.OK);
- }
-
- @PostMapping(value = "/decodetracks", consumes = "application/json", produces = "application/json")
- @ResponseBody
- public ResponseEntity postDecodeTracks(HttpServletRequest request, @RequestBody String body)
- throws IOException {
-
- log(request);
-
- JSONArray requestJSON = new JSONArray(body);
- JSONArray responseJSON = new JSONArray();
-
- for (int i = 0; i < requestJSON.length(); i++) {
- String track = requestJSON.getString(i);
- AudioTrack audioTrack = Util.toAudioTrack(audioPlayerManager, track);
-
- JSONObject infoJSON = trackToJSON(audioTrack);
- JSONObject trackJSON = new JSONObject()
- .put("track", track)
- .put("info", infoJSON);
-
- responseJSON.put(trackJSON);
- }
-
- return new ResponseEntity<>(responseJSON.toString(), HttpStatus.OK);
- }
-}
diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.kt b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.kt
new file mode 100644
index 000000000..291384df7
--- /dev/null
+++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2021 Freya Arbjerg and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package lavalink.server.player
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.JsonNodeFactory
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager
+import dev.arbjerg.lavalink.protocol.v3.Track
+import dev.arbjerg.lavalink.protocol.v3.decodeTrack
+import lavalink.server.util.toTrack
+import org.slf4j.LoggerFactory
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+import org.springframework.web.server.ResponseStatusException
+import java.util.concurrent.CompletionStage
+import javax.servlet.http.HttpServletRequest
+
+@RestController
+class AudioLoaderRestHandler(
+ private val audioPlayerManager: AudioPlayerManager,
+ private val objectMapper: ObjectMapper
+) {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(AudioLoaderRestHandler::class.java)
+ }
+
+ @GetMapping(value = ["/loadtracks", "/v3/loadtracks"])
+ fun loadTracks(
+ request: HttpServletRequest,
+ @RequestParam identifier: String
+ ): CompletionStage> {
+ log.info("Got request to load for identifier \"${identifier}\"")
+ return AudioLoader(audioPlayerManager).load(identifier).thenApply {
+ val node: ObjectNode = objectMapper.valueToTree(it)
+ if (request.servletPath.startsWith("/loadtracks") || request.servletPath.startsWith("/v3/loadtracks")) {
+ if (node.get("playlistInfo").isNull) {
+ node.replace("playlistInfo", JsonNodeFactory.instance.objectNode())
+ }
+
+ if (node.get("exception").isNull) {
+ node.remove("exception")
+ }
+ }
+
+ return@thenApply ResponseEntity.ok(node)
+ }
+ }
+
+ @GetMapping(value = ["/decodetrack", "/v3/decodetrack"])
+ fun getDecodeTrack(@RequestParam encodedTrack: String?, @RequestParam track: String?): ResponseEntity