diff --git a/CHANGELOG.md b/CHANGELOG.md index 022c14832..f35821192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,29 @@ Each release usually includes various fixes and improvements. The most noteworthy of these, as well as any features and breaking changes, are listed here. +## v3.1.2 +* Add API version header to all responses + +Contributor: +[@Devoxin](https://github.com/Devoxin) + +## v3.1.1 +* Add equalizer support +* Update lavaplayer to 1.3.10 +* Fixed automatic versioning +* Added build config to upload binaries to GitHub releases from CI + +Contributors: +[@Devoxin](https://github.com/Devoxin), +[@Frederikam](https://github.com/Frederikam/), +[@calebj](https://github.com/calebj) + +## v3.1 +* Replaced JDAA with Magma +* Added an event for when the Discord voice WebSocket is closed +* Replaced Tomcat and Java_Websocket with Undertow. WS and REST is now handled by the same +server and port. Port is specified by `server.port`. + ## v3.0 * **Breaking:** The minimum required Java version to run the server is now Java 10. **Please note**: Java 10 will be obsolete diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index e71e5c29c..637257ea9 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -110,6 +110,24 @@ Set player volume. Volume may range from 0 to 1000. 100 is default. } ``` +Using the player equalizer +```json +{ + "op": "equalizer", + "guildId": "...", + "bands": [ + { + "band": 0, + "gain": 0.2 + } + ] +} +``` +There are 15 bands (0-14) that can be changed. +`gain` is the multiplier for the given band. The default value is 0. Valid values range from -0.25 to 1.0, +where -0.25 means the given band is completely muted, and 0.25 means it is doubled. Modifying the gain could +also change the volume of the output. + 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. @@ -289,6 +307,8 @@ Additionally, in every `/loadtracks` response, a `loadType` property is returned * `NO_MATCHES` - Returned if no matches/sources could be found for a given identifier. * `LOAD_FAILED` - Returned if Lavaplayer failed to load something for some reason. +All REST responses from Lavalink include a `Lavalink-Api-Version` header. + ### Resuming Lavalink sessions What happens after your client disconnects is dependent on whether or not the session has been configured for resuming. diff --git a/LavalinkServer/application.yml.example b/LavalinkServer/application.yml.example index f7152c23d..1dd30fc54 100644 --- a/LavalinkServer/application.yml.example +++ b/LavalinkServer/application.yml.example @@ -17,7 +17,7 @@ lavalink: http: true local: false bufferDurationMs: 400 - youtubePlaylistLoadLimit: 600 + youtubePlaylistLoadLimit: 6 # Number of pages at 100 each gc-warnings: true metrics: diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index ed29defd4..d0a7e639c 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -22,7 +22,7 @@ sourceCompatibility = 10 targetCompatibility = 10 bootRun { - //compiling tests during bootRun increases the likelyhood of catching broken tests locally instead of on the CI + //compiling tests during bootRun increases the likelihood of catching broken tests locally instead of on the CI dependsOn compileTestJava //pass in custom jvm args @@ -97,10 +97,16 @@ compileTestKotlin { String versionFromTag() { def headTag = grgit.tag.list().find { - it.commit == grgit.head() + it.commit.getId() == grgit.head().getId() } - def clean = grgit.status().clean //uncommitted changes? -> should be SNAPSHOT + // Uncommitted changes? -> should be SNAPSHOT + // Also watch out for false positives in the CI build + def clean = grgit.status().clean || System.getenv('CI') != null + + if (!clean) { + println("Git state is dirty, setting version as snapshot") + } if (headTag && clean) { headTag.getName() diff --git a/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java index 3ea5eb4af..dd1082b20 100644 --- a/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java +++ b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java @@ -45,6 +45,8 @@ public Supplier audioPlayerManagerSupplier(AudioSourcesConfi if (sources.isHttp()) audioPlayerManager.registerSourceManager(new HttpAudioSourceManager()); if (sources.isLocal()) audioPlayerManager.registerSourceManager(new LocalAudioSourceManager()); + audioPlayerManager.getConfiguration().setFilterHotSwapEnabled(true); + return audioPlayerManager; }; } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java b/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java new file mode 100644 index 000000000..4fcb95e5a --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/io/ResponseHeaderFilter.java @@ -0,0 +1,22 @@ +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/SocketContext.kt b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt index b0c2a7b46..7371dbd1b 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.kt @@ -23,21 +23,25 @@ package lavalink.server.io import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager +import lavalink.server.player.Player +import space.npstr.magma.MagmaMember 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.player.Player import org.json.JSONObject import org.slf4j.LoggerFactory import org.springframework.web.socket.WebSocketSession import org.springframework.web.socket.adapter.standard.StandardWebSocketSession import space.npstr.magma.MagmaApi -import space.npstr.magma.MagmaMember import space.npstr.magma.events.api.MagmaEvent import space.npstr.magma.events.api.WebSocketClosed import java.util.* import java.util.concurrent.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit import java.util.function.Supplier class SocketContext internal constructor( @@ -55,8 +59,6 @@ class SocketContext internal constructor( internal val magma: MagmaApi = MagmaApi.of { socketServer.getAudioSendFactory(it) } //guildId <-> Player val players = ConcurrentHashMap() - private val executor: ScheduledExecutorService - val playerUpdateService: ScheduledExecutorService @Volatile var sessionPaused = false private val resumeEventQueue = ConcurrentLinkedQueue() @@ -65,6 +67,8 @@ class SocketContext internal constructor( var resumeKey: String? = null var resumeTimeout = 60L // Seconds private var sessionTimeoutFuture: ScheduledFuture? = null + private val executor: ScheduledExecutorService + val playerUpdateService: ScheduledExecutorService val playingPlayers: List get() { @@ -92,6 +96,10 @@ class SocketContext internal constructor( Player(this, guildId, audioPlayerManager) } + internal fun getPlayers(): Map { + return players + } + private fun handleMagmaEvent(magmaEvent: MagmaEvent) { if (magmaEvent is WebSocketClosed) { val out = JSONObject() diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt index 4cff9a2eb..cbf424220 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.kt @@ -25,9 +25,11 @@ package lavalink.server.io import com.github.shredder121.asyncaudio.jda.AsyncPacketProviderFactory import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager +import com.sedmelluq.discord.lavaplayer.track.TrackMarker import lavalink.server.config.AudioSendFactoryConfiguration import lavalink.server.config.ServerConfig import lavalink.server.player.Player +import lavalink.server.player.TrackEndMarkerHandler import lavalink.server.util.Util import net.dv8tion.jda.core.audio.factory.IAudioSendFactory import org.json.JSONObject @@ -38,7 +40,7 @@ import org.springframework.web.socket.TextMessage import org.springframework.web.socket.WebSocketSession import org.springframework.web.socket.handler.TextWebSocketHandler import space.npstr.magma.Member -import java.util.* +import java.util.HashMap import java.util.concurrent.ConcurrentHashMap import java.util.function.Supplier @@ -54,7 +56,7 @@ class SocketServer( val contextMap = HashMap() private val sendFactories = ConcurrentHashMap() @Suppress("LeakingThis") - private val handlers = WebSocketHandlers(this) + private val handlers = WebSocketHandlers(contextMap) private val resumableSessions = mutableMapOf() companion object { @@ -90,6 +92,8 @@ class SocketServer( return } + shardCounts[userId] = shardCount + contextMap[session.id] = SocketContext(audioPlayerManagerSupplier, session, this, userId) log.info("Connection successfully established from " + session.remoteAddress!!) } @@ -149,6 +153,7 @@ class SocketServer( "volume" -> handlers.volume(session, json) "destroy" -> handlers.destroy(session, json) "configureResuming" -> handlers.configureResuming(session, json) + "equalizer" -> handlers.equalizer(session, json) else -> log.warn("Unexpected operation: " + json.getString("op")) // @formatter:on } @@ -158,8 +163,8 @@ class SocketServer( val shardCount = shardCounts.getOrDefault(member.userId, 1) val shardId = Util.getShardFromSnowflake(member.guildId, shardCount) - return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.audioSendFactoryCount) - { + return sendFactories.computeIfAbsent(shardId % audioSendFactoryConfiguration.audioSendFactoryCount + ) { val customBuffer = serverConfig.bufferDurationMs val nativeAudioSendFactory: NativeAudioSendFactory nativeAudioSendFactory = if (customBuffer != null) { diff --git a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java index 1ccb237f6..aa3c51ea8 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/StatsTask.java @@ -89,7 +89,9 @@ private void sendStats() { JSONObject cpu = new JSONObject(); cpu.put("cores", Runtime.getRuntime().availableProcessors()); cpu.put("systemLoad", hal.getProcessor().getSystemCpuLoad()); - cpu.put("lavalinkLoad", getProcessRecentCpuUsage()); + double load = getProcessRecentCpuUsage(); + if (!Double.isFinite(load)) load = 0; + cpu.put("lavalinkLoad", load); out.put("cpu", cpu); diff --git a/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandlers.kt b/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandlers.kt index 8bdefa52c..b67411dd9 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandlers.kt +++ b/LavalinkServer/src/main/java/lavalink/server/io/WebSocketHandlers.kt @@ -8,9 +8,7 @@ import org.springframework.web.socket.WebSocketSession import space.npstr.magma.MagmaMember import space.npstr.magma.MagmaServerUpdate -class WebSocketHandlers(socketServer: SocketServer) { - - private val contextMap = socketServer.contextMap +class WebSocketHandlers(private val contextMap: Map) { fun voiceUpdate(session: WebSocketSession, json: JSONObject) { val sessionId = json.getString("sessionId") @@ -91,6 +89,16 @@ class WebSocketHandlers(socketServer: SocketServer) { player.setVolume(json.getInt("volume")) } + fun equalizer(session: WebSocketSession, json: JSONObject) { + val player = contextMap[session.id]!!.getPlayer(json.getString("guildId")) + val bands = json.getJSONArray("bands") + + for (i in 0 until bands.length()) { + val band = bands.getJSONObject(i) + player.setBandGain(band.getInt("band"), band.getFloat("gain")) + } + } + fun destroy(session: WebSocketSession, json: JSONObject) { val socketContext = contextMap[session.id]!! val player = socketContext.players.remove(json.getString("guildId")) @@ -108,5 +116,4 @@ class WebSocketHandlers(socketServer: SocketServer) { socketContext.resumeKey = json.optString("key") if (json.has("timeout")) socketContext.resumeTimeout = json.getLong("timeout") } - } \ No newline at end of file diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index 1fc415c44..b61c55120 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -22,6 +22,8 @@ package lavalink.server.player; +import com.sedmelluq.discord.lavaplayer.filter.equalizer.Equalizer; +import com.sedmelluq.discord.lavaplayer.filter.equalizer.EqualizerFactory; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; @@ -49,6 +51,8 @@ public class Player extends AudioEventAdapter implements AudioSendHandler { private AudioLossCounter audioLossCounter = new AudioLossCounter(); private AudioFrame lastFrame = null; private ScheduledFuture myFuture = null; + private EqualizerFactory equalizerFactory = new EqualizerFactory(); + private boolean isEqualizerApplied = false; public Player(SocketContext socketContext, String guildId, AudioPlayerManager audioPlayerManager) { this.socketContext = socketContext; @@ -87,6 +91,33 @@ public void setVolume(int volume) { player.setVolume(volume); } + public void setBandGain(int band, float gain) { + log.debug("Setting band {}'s gain to {}", band, gain); + equalizerFactory.setGain(band, gain); + + if (gain == 0.0f) { + if (!isEqualizerApplied) { + return; + } + + boolean shouldDisable = true; + + for (int i = 0; i < Equalizer.BAND_COUNT; i++) { + if (equalizerFactory.getGain(i) != 0.0f) { + shouldDisable = false; + } + } + + if (shouldDisable) { + this.player.setFilterFactory(null); + this.isEqualizerApplied = false; + } + } else if (!this.isEqualizerApplied) { + this.player.setFilterFactory(equalizerFactory); + this.isEqualizerApplied = true; + } + } + public JSONObject getState() { JSONObject json = new JSONObject(); diff --git a/README.md b/README.md index 517c9facc..dba5651aa 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ users about the compatibility of their clients to the Lavalink server. * [SandySounds](https://github.com/MrJohnCoder/SandySounds) (JavaScript) * [eris-lavalink](https://github.com/briantanner/eris-lavalink) ([eris](https://github.com/abalabahaha/eris), JavaScript) * [discord.js-lavalink](https://github.com/MrJacz/discord.js-lavalink/) ([discord.js](https://github.com/discordjs/discord.js), JavaScript) -* [Victoria](https://github.com/Yucked/Bot.NET/tree/master/Victoria) ([Discord.NET](https://github.com/RogueException/Discord.Net), .NET) +* [SharpLink](https://github.com/Devoxin/SharpLink) ([Discord.Net](https://github.com/RogueException/Discord.Net), .NET) +* [Victoria](https://github.com/Yucked/Victoria) ([Discord.NET](https://github.com/RogueException/Discord.Net), .NET) * [Lavalink.NET](https://github.com/Dev-Yukine/Lavalink.NET) (.NET) * [DSharpPlus.Lavalink](https://github.com/DSharpPlus/DSharpPlus/tree/master/DSharpPlus.Lavalink) ([DSharpPlus](https://github.com/DSharpPlus/DSharpPlus/), .NET) * [Luna](https://github.com/CharlotteDunois/Luna) ([Yasmin](https://github.com/CharlotteDunois/Yasmin) or generic, PHP) @@ -79,7 +80,7 @@ users about the compatibility of their clients to the Lavalink server. * Or [create your own](https://github.com/Frederikam/Lavalink/blob/master/IMPLEMENTATION.md) ## Server configuration -Download from [the CI server](https://ci.fredboat.com/viewLog.html?buildId=lastSuccessful&buildTypeId=Lavalink_Build&tab=artifacts&guest=1) +Download binaries from [the CI server](https://ci.fredboat.com/viewLog.html?buildId=lastSuccessful&buildTypeId=Lavalink_Build&tab=artifacts&guest=1) or [the GitHub releases](https://github.com/Frederikam/Lavalink/releases). Put an `application.yml` file in your working directory. [Example](https://github.com/Frederikam/Lavalink/blob/master/LavalinkServer/application.yml.example) diff --git a/build.gradle b/build.gradle index e2d9e6bcc..eb07d24e8 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ subprojects { ext { //@formatter:off - lavaplayerVersion = '1.3.7' + lavaplayerVersion = '1.3.10' magmaVersion = '0.6.1' jdaNasVersion = '1.0.6' jappVersion = '1.3'