diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..5fd5f4e79 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: java + +addons: + sonarcloud: + organization: "fredboat" + +env: + global: + - BUILD_NUMBER: "$TRAVIS_BUILD_NUMBER" + +cache: + directories: + - "$HOME/.m2" + - "$HOME/.gradle" + - ".gradle/wrapper" + - ".gradle/caches" + - "$HOME/.sonar/cache" + +stages: + - sonar + +jobs: + fast_finish: true + include: + - stage: sonar + jdk: openjdk8 + if: env(TRAVIS_SECURE_ENV_VARS) = true + before_script: + #for full sonar cloud blame information + - "git fetch --unshallow" + script: + - "./gradlew sonarqube -PincludeAnalysis" diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index a496c1432..b803a5720 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -95,7 +95,7 @@ Make the player seek to a position of the track. Position is in millis } ``` -Set player volume. Volume may range from 0 to 150. 100 is default. +Set player volume. Volume may range from 0 to 1000. 100 is default. ```json { "op": "volume", diff --git a/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java b/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java index 4e0bd2932..0e4e73177 100644 --- a/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java +++ b/LavalinkClient/src/main/java/lavalink/client/player/LavalinkPlayer.java @@ -171,7 +171,7 @@ public void seekTo(long position) { @Override public void setVolume(int volume) { - volume = Math.min(150, Math.max(0, volume)); // Lavaplayer bounds + volume = Math.min(1000, Math.max(0, volume)); // Lavaplayer bounds this.volume = volume; LavalinkSocket node = link.getNode(false); diff --git a/LavalinkServer/application.yml.example b/LavalinkServer/application.yml.example index fb119f26b..d6a95cd55 100644 --- a/LavalinkServer/application.yml.example +++ b/LavalinkServer/application.yml.example @@ -1,6 +1,9 @@ server: # REST server port: 2333 address: 0.0.0.0 +spring: + main: + banner-mode: log lavalink: server: password: "youshallnotpass" diff --git a/LavalinkServer/build.gradle b/LavalinkServer/build.gradle index 93cdce836..8fbc79e3f 100644 --- a/LavalinkServer/build.gradle +++ b/LavalinkServer/build.gradle @@ -1,10 +1,12 @@ +import org.apache.tools.ant.filters.ReplaceTokens + apply plugin: 'application' apply plugin: 'org.springframework.boot' apply plugin: 'com.gorylenko.gradle-git-properties' description = 'Play audio to discord voice channels' mainClassName = "lavalink.server.Launcher" -version '2.1' +version '2.2' ext { moduleName = 'Lavalink-Server' } @@ -25,6 +27,10 @@ bootRun { } } +test { + useJUnitPlatform() +} + dependencies { compile group: 'com.sedmelluq', name: 'lavaplayer', version: lavaplayerVersion compile group: 'com.github.DV8FromTheWorld', name: 'JDA-Audio', version: jdaAudioVersion @@ -45,6 +51,19 @@ dependencies { compile group: 'io.prometheus', name: 'simpleclient_servlet', version: prometheusVersion } +processResources { + //inject values into app.properties + filesMatching("**/app.properties") { + filter ReplaceTokens, tokens: [ + "project.version" : project.version, + "project.groupId" : project.group, + "project.artifactId": project.ext.moduleName, + "env.BUILD_NUMBER" : (System.getenv('CI') ? System.getenv('BUILD_NUMBER') : 'DEV'), + "env.BUILD_TIME" : System.currentTimeMillis() + '' + ] + } +} + //create a simple version file that we will be reading to create appropriate docker tags void versionTxt() { new File("$projectDir/VERSION.txt").text = "$project.version\n" diff --git a/LavalinkServer/src/main/java/lavalink/server/Launcher.java b/LavalinkServer/src/main/java/lavalink/server/Launcher.java index aa353ed55..e6368cd97 100644 --- a/LavalinkServer/src/main/java/lavalink/server/Launcher.java +++ b/LavalinkServer/src/main/java/lavalink/server/Launcher.java @@ -23,10 +23,13 @@ package lavalink.server; import ch.qos.logback.classic.LoggerContext; +import com.sedmelluq.discord.lavaplayer.tools.PlayerLibrary; import io.sentry.Sentry; import io.sentry.SentryClient; import io.sentry.logback.SentryAppender; import lavalink.server.config.ServerConfig; +import lavalink.server.info.AppInfo; +import lavalink.server.info.GitRepoState; import lavalink.server.io.SocketServer; import lavalink.server.util.SimpleLogToSLF4JAdapter; import net.dv8tion.jda.utils.SimpleLog; @@ -35,9 +38,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; +import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.context.annotation.ComponentScan; import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Properties; @SpringBootApplication @@ -49,8 +57,29 @@ public class Launcher { public final static long startTime = System.currentTimeMillis(); public static void main(String[] args) { + if (args.length > 0 + && (args[0].equalsIgnoreCase("-v") + || args[0].equalsIgnoreCase("--version"))) { + System.out.println("Version flag detected. Printing version info, then exiting."); + System.out.println(getVersionInfo()); + return; + } + SpringApplication sa = new SpringApplication(Launcher.class); sa.setWebApplicationType(WebApplicationType.SERVLET); + sa.addListeners( + event -> { + if (event instanceof ApplicationEnvironmentPreparedEvent) { + log.info(getVersionInfo()); + } + }, + event -> { + if (event instanceof ApplicationFailedEvent) { + ApplicationFailedEvent failed = (ApplicationFailedEvent) event; + log.error("Application failed", failed.getException()); + } + } + ); sa.run(args); } @@ -102,4 +131,46 @@ private void turnOffSentry() { Sentry.close(); sentryAppender.stop(); } + + private static String getVersionInfo() { + AppInfo appInfo = new AppInfo(); + GitRepoState gitRepoState = new GitRepoState(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss z") + .withZone(ZoneId.of("Europe/Copenhagen")); + String buildTime = dtf.format(Instant.ofEpochMilli(appInfo.getBuildTime())); + String commitTime = dtf.format(Instant.ofEpochMilli(gitRepoState.getCommitTime() * 1000)); + + return "\n\n" + getVanity() + + "\n" + + "\n\tVersion: " + appInfo.getVersion() + + "\n\tBuild: " + appInfo.getBuildNumber() + + "\n\tBuild time: " + buildTime + + "\n\tBranch " + gitRepoState.getBranch() + + "\n\tCommit: " + gitRepoState.getCommitIdAbbrev() + + "\n\tCommit time: " + commitTime + + "\n\tJVM: " + System.getProperty("java.version") + + "\n\tLavaplayer " + PlayerLibrary.VERSION + + "\n"; + } + + private static String getVanity() { + //ansi color codes + String red = ""; + String green = ""; + String defaultC = ""; + + String vanity + = "g . r _ _ _ _ g__ _ _\n" + + "g /\\\\ r| | __ ___ ____ _| (_)_ __ | | __g\\ \\ \\ \\\n" + + "g ( ( )r| |/ _` \\ \\ / / _` | | | '_ \\| |/ /g \\ \\ \\ \\\n" + + "g \\\\/ r| | (_| |\\ V / (_| | | | | | | < g ) ) ) )\n" + + "g ' r|_|\\__,_| \\_/ \\__,_|_|_|_| |_|_|\\_\\g / / / /\n" + + "d =========================================g/_/_/_/d"; + + vanity = vanity.replaceAll("r", red); + vanity = vanity.replaceAll("g", green); + vanity = vanity.replaceAll("d", defaultC); + return vanity; + } } diff --git a/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java index 2b74f028a..3ea5eb4af 100644 --- a/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java +++ b/LavalinkServer/src/main/java/lavalink/server/config/AudioPlayerConfiguration.java @@ -13,6 +13,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; +import java.util.function.Supplier; + /** * Created by napster on 05.03.18. */ @@ -20,30 +22,36 @@ public class AudioPlayerConfiguration { @Bean - public AudioPlayerManager audioPlayerManager(AudioSourcesConfig sources, ServerConfig serverConfig) { - - AudioPlayerManager audioPlayerManager = new DefaultAudioPlayerManager(); - - if (serverConfig.isGcWarnings()) { - audioPlayerManager.enableGcMonitoring(); - } - - if (sources.isYoutube()) { - YoutubeAudioSourceManager youtube = new YoutubeAudioSourceManager(); - Integer playlistLoadLimit = serverConfig.getYoutubePlaylistLoadLimit(); - - if (playlistLoadLimit != null) youtube.setPlaylistPageCount(playlistLoadLimit); - audioPlayerManager.registerSourceManager(youtube); - } - if (sources.isBandcamp()) audioPlayerManager.registerSourceManager(new BandcampAudioSourceManager()); - if (sources.isSoundcloud()) audioPlayerManager.registerSourceManager(new SoundCloudAudioSourceManager()); - if (sources.isTwitch()) audioPlayerManager.registerSourceManager(new TwitchStreamAudioSourceManager()); - if (sources.isVimeo()) audioPlayerManager.registerSourceManager(new VimeoAudioSourceManager()); - if (sources.isMixer()) audioPlayerManager.registerSourceManager(new BeamAudioSourceManager()); - if (sources.isHttp()) audioPlayerManager.registerSourceManager(new HttpAudioSourceManager()); - if (sources.isLocal()) audioPlayerManager.registerSourceManager(new LocalAudioSourceManager()); - - return audioPlayerManager; + public Supplier audioPlayerManagerSupplier(AudioSourcesConfig sources, ServerConfig serverConfig) { + return () -> { + AudioPlayerManager audioPlayerManager = new DefaultAudioPlayerManager(); + + if (serverConfig.isGcWarnings()) { + audioPlayerManager.enableGcMonitoring(); + } + + if (sources.isYoutube()) { + YoutubeAudioSourceManager youtube = new YoutubeAudioSourceManager(); + Integer playlistLoadLimit = serverConfig.getYoutubePlaylistLoadLimit(); + + if (playlistLoadLimit != null) youtube.setPlaylistPageCount(playlistLoadLimit); + audioPlayerManager.registerSourceManager(youtube); + } + if (sources.isBandcamp()) audioPlayerManager.registerSourceManager(new BandcampAudioSourceManager()); + if (sources.isSoundcloud()) audioPlayerManager.registerSourceManager(new SoundCloudAudioSourceManager()); + if (sources.isTwitch()) audioPlayerManager.registerSourceManager(new TwitchStreamAudioSourceManager()); + if (sources.isVimeo()) audioPlayerManager.registerSourceManager(new VimeoAudioSourceManager()); + if (sources.isMixer()) audioPlayerManager.registerSourceManager(new BeamAudioSourceManager()); + if (sources.isHttp()) audioPlayerManager.registerSourceManager(new HttpAudioSourceManager()); + if (sources.isLocal()) audioPlayerManager.registerSourceManager(new LocalAudioSourceManager()); + + return audioPlayerManager; + }; + } + + @Bean + public AudioPlayerManager restAudioPlayerManager(Supplier supplier) { + return supplier.get(); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java new file mode 100644 index 000000000..0143e13de --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/info/AppInfo.java @@ -0,0 +1,65 @@ +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 String buildNumber; + 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"); + this.buildNumber = prop.getProperty("buildNumber"); + this.buildTime = Long.parseLong(prop.getProperty("buildTime")); + } + + public String getVersion() { + return this.version; + } + + public String getGroupId() { + return this.groupId; + } + + public String getArtifactId() { + return this.artifactId; + } + + public String getBuildNumber() { + return this.buildNumber; + } + + public long getBuildTime() { + return this.buildTime; + } + + public String getVersionBuild() { + return this.version + "_" + this.buildNumber; + } +} diff --git a/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java new file mode 100644 index 000000000..0c1dcdf01 --- /dev/null +++ b/LavalinkServer/src/main/java/lavalink/server/info/GitRepoState.java @@ -0,0 +1,97 @@ +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 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")); + } catch (NullPointerException e) { + log.info("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) { + 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; + } +} + diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java index dea1078a0..da276ef14 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketContext.java @@ -45,6 +45,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; public class SocketContext { @@ -62,10 +63,10 @@ public class SocketContext { public final ScheduledExecutorService playerUpdateService; private final ConcurrentHashMap sendFactories = new ConcurrentHashMap<>(); - SocketContext(AudioPlayerManager audioPlayerManager, ServerConfig serverConfig, WebSocket socket, + SocketContext(Supplier audioPlayerManagerSupplier, ServerConfig serverConfig, WebSocket socket, AudioSendFactoryConfiguration audioSendFactoryConfiguration, SocketServer socketServer, String userId, int shardCount) { - this.audioPlayerManager = audioPlayerManager; + this.audioPlayerManager = audioPlayerManagerSupplier.get(); this.serverConfig = serverConfig; this.socket = socket; this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; @@ -123,6 +124,7 @@ List getPlayingPlayers() { void shutdown() { log.info("Shutting down " + cores.size() + " cores and " + getPlayingPlayers().size() + " playing players."); statsExecutor.shutdown(); + audioPlayerManager.shutdown(); playerUpdateService.shutdown(); players.keySet().forEach(s -> { Core core = cores.get(Util.getShardFromSnowflake(s, shardCount)); @@ -152,4 +154,7 @@ private IAudioSendFactory getAudioSendFactory(int shardId) { }); } + public AudioPlayerManager getAudioPlayerManager() { + return audioPlayerManager; + } } diff --git a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java index f05216948..c28b3c5bd 100644 --- a/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java +++ b/LavalinkServer/src/main/java/lavalink/server/io/SocketServer.java @@ -47,6 +47,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; import static lavalink.server.io.WSCodes.AUTHORIZATION_REJECTED; import static lavalink.server.io.WSCodes.INTERNAL_ERROR; @@ -58,15 +59,15 @@ public class SocketServer extends WebSocketServer { private final Map contextMap = new HashMap<>(); private final ServerConfig serverConfig; - private final AudioPlayerManager audioPlayerManager; + private final Supplier audioPlayerManagerSupplier; private final AudioSendFactoryConfiguration audioSendFactoryConfiguration; - public SocketServer(WebsocketConfig websocketConfig, ServerConfig serverConfig, AudioPlayerManager audioPlayerManager, + public SocketServer(WebsocketConfig websocketConfig, ServerConfig serverConfig, Supplier audioPlayerManagerSupplier, AudioSendFactoryConfiguration audioSendFactoryConfiguration) { super(new InetSocketAddress(websocketConfig.getHost(), websocketConfig.getPort())); this.setReuseAddr(true); this.serverConfig = serverConfig; - this.audioPlayerManager = audioPlayerManager; + this.audioPlayerManagerSupplier = audioPlayerManagerSupplier; this.audioSendFactoryConfiguration = audioSendFactoryConfiguration; } @@ -84,7 +85,7 @@ public void onOpen(WebSocket webSocket, ClientHandshake clientHandshake) { if (clientHandshake.getFieldValue("Authorization").equals(serverConfig.getPassword())) { log.info("Connection opened from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); - contextMap.put(webSocket, new SocketContext(audioPlayerManager, serverConfig, webSocket, + contextMap.put(webSocket, new SocketContext(audioPlayerManagerSupplier, serverConfig, webSocket, audioSendFactoryConfiguration, this, userId, shardCount)); } else { log.error("Authentication failed from " + webSocket.getRemoteSocketAddress() + " with protocol " + webSocket.getDraft()); @@ -146,8 +147,9 @@ public void onMessage(WebSocket webSocket, String s) { /* Player ops */ case "play": try { - Player player = contextMap.get(webSocket).getPlayer(json.getString("guildId")); - AudioTrack track = Util.toAudioTrack(audioPlayerManager, json.getString("track")); + SocketContext ctx = contextMap.get(webSocket); + Player player = ctx.getPlayer(json.getString("guildId")); + AudioTrack track = Util.toAudioTrack(ctx.getAudioPlayerManager(), json.getString("track")); if (json.has("startTime")) { track.setPosition(json.getLong("startTime")); } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java index 272252a3b..d5ce3df8f 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoader.java @@ -31,70 +31,61 @@ 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 final AudioPlayerManager audioPlayerManager; - private List loadedItems; - private boolean used = false; + private final CompletableFuture> loadedItems; + private final AtomicBoolean used = new AtomicBoolean(false); public AudioLoader(AudioPlayerManager audioPlayerManager) { this.audioPlayerManager = audioPlayerManager; + this.loadedItems = new CompletableFuture<>(); } - List loadSync(String identifier) throws InterruptedException { - if(used) + 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"); - - used = true; - - audioPlayerManager.loadItem(identifier, this); - - synchronized (this) { - this.wait(); } + log.trace("Loading item with identifier {}", identifier); + this.audioPlayerManager.loadItem(identifier, this); + return loadedItems; } @Override public void trackLoaded(AudioTrack audioTrack) { - loadedItems = new ArrayList<>(); - loadedItems.add(audioTrack); log.info("Loaded track " + audioTrack.getInfo().title); - synchronized (this) { - this.notify(); - } + ArrayList result = new ArrayList<>(); + result.add(audioTrack); + this.loadedItems.complete(result); } @Override public void playlistLoaded(AudioPlaylist audioPlaylist) { log.info("Loaded playlist " + audioPlaylist.getName()); - loadedItems = audioPlaylist.getTracks(); - synchronized (this) { - this.notify(); - } + this.loadedItems.complete(audioPlaylist.getTracks()); } @Override public void noMatches() { log.info("No matches found"); - loadedItems = new ArrayList<>(); - synchronized (this) { - this.notify(); - } + this.loadedItems.complete(Collections.emptyList()); } @Override public void loadFailed(FriendlyException e) { log.error("Load failed", e); - loadedItems = new ArrayList<>(); - synchronized (this) { - this.notify(); - } + this.loadedItems.complete(Collections.emptyList()); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java index 040df6188..9aa2cc281 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/AudioLoaderRestHandler.java @@ -31,19 +31,24 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Controller; +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 javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; -@Controller +@RestController public class AudioLoaderRestHandler { private static final Logger log = LoggerFactory.getLogger(AudioLoaderRestHandler.class); @@ -60,18 +65,18 @@ private void log(HttpServletRequest request) { log.info("GET " + path); } - private boolean isAuthorized(HttpServletRequest request, HttpServletResponse response) { + //returns an empty answer if the auth succeeded, or a response to send back immediately + private Optional> checkAuthorization(HttpServletRequest request) { if (request.getHeader("Authorization") == null) { - response.setStatus(403); - return false; + return Optional.of(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); } if (!request.getHeader("Authorization").equals(serverConfig.getPassword())) { log.warn("Authorization failed"); - response.setStatus(403); - return false; + return Optional.of(new ResponseEntity<>(HttpStatus.FORBIDDEN)); } - return true; + + return Optional.empty(); } private JSONObject trackToJSON(AudioTrack audioTrack) { @@ -88,19 +93,9 @@ private JSONObject trackToJSON(AudioTrack audioTrack) { .put("position", audioTrack.getPosition()); } - @GetMapping(value = "/loadtracks", produces = "application/json") - @ResponseBody - public String getLoadTracks(HttpServletRequest request, HttpServletResponse response, @RequestParam String identifier) - throws IOException, InterruptedException { - log(request); - - if (!isAuthorized(request, response)) - return ""; - + private JSONArray encodeTrackList(List trackList) { JSONArray tracks = new JSONArray(); - List list = new AudioLoader(audioPlayerManager).loadSync(identifier); - - list.forEach(track -> { + trackList.forEach(track -> { JSONObject object = new JSONObject(); object.put("info", trackToJSON(track)); @@ -109,33 +104,53 @@ public String getLoadTracks(HttpServletRequest request, HttpServletResponse resp object.put("track", encoded); tracks.put(object); } catch (IOException e) { - throw new RuntimeException(); + log.warn("Failed to encode a track {}, skipping", track.getIdentifier(), e); } }); + return tracks; + } - return tracks.toString(); + @GetMapping(value = "/loadtracks", produces = "application/json") + @ResponseBody + public CompletionStage> getLoadTracks(HttpServletRequest request, @RequestParam String identifier) { + log(request); + + Optional> notAuthed = checkAuthorization(request); + if (notAuthed.isPresent()) { + return CompletableFuture.completedFuture(notAuthed.get()); + } + + return new AudioLoader(audioPlayerManager).load(identifier) + .thenApply(this::encodeTrackList) + .thenApply(tracksArray -> new ResponseEntity<>(tracksArray.toString(), HttpStatus.OK)); } @GetMapping(value = "/decodetrack", produces = "application/json") @ResponseBody - public String getDecodeTrack(HttpServletRequest request, HttpServletResponse response, @RequestParam String track) throws IOException { + public ResponseEntity getDecodeTrack(HttpServletRequest request, HttpServletResponse response, @RequestParam String track) + throws IOException { log(request); - if (!isAuthorized(request, response)) - return ""; + Optional> notAuthed = checkAuthorization(request); + if (notAuthed.isPresent()) { + return notAuthed.get(); + } AudioTrack audioTrack = Util.toAudioTrack(audioPlayerManager, track); - return trackToJSON(audioTrack).toString(); + return new ResponseEntity<>(trackToJSON(audioTrack).toString(), HttpStatus.OK); } @PostMapping(value = "/decodetracks", consumes = "application/json", produces = "application/json") @ResponseBody - public String postDecodeTracks(HttpServletRequest request, HttpServletResponse response, @RequestBody String body) throws IOException { + public ResponseEntity postDecodeTracks(HttpServletRequest request, HttpServletResponse response, @RequestBody String body) + throws IOException { log(request); - if (!isAuthorized(request, response)) - return ""; + Optional> notAuthed = checkAuthorization(request); + if (notAuthed.isPresent()) { + return notAuthed.get(); + } JSONArray requestJSON = new JSONArray(body); JSONArray responseJSON = new JSONArray(); @@ -152,6 +167,6 @@ public String postDecodeTracks(HttpServletRequest request, HttpServletResponse r responseJSON.put(trackJSON); } - return responseJSON.toString(); + return new ResponseEntity<>(responseJSON.toString(), HttpStatus.OK); } } diff --git a/LavalinkServer/src/main/java/lavalink/server/player/Player.java b/LavalinkServer/src/main/java/lavalink/server/player/Player.java index 53f14a3f4..67af5313c 100644 --- a/LavalinkServer/src/main/java/lavalink/server/player/Player.java +++ b/LavalinkServer/src/main/java/lavalink/server/player/Player.java @@ -111,7 +111,7 @@ public boolean canProvide() { @Override public byte[] provide20MsAudio() { - return lastFrame.data; + return lastFrame.getData(); } @Override diff --git a/LavalinkServer/src/main/resources/app.properties b/LavalinkServer/src/main/resources/app.properties new file mode 100644 index 000000000..eaa427542 --- /dev/null +++ b/LavalinkServer/src/main/resources/app.properties @@ -0,0 +1,5 @@ +version=@project.version@ +groupId=@project.groupId@ +artifactId=@project.artifactId@ +buildNumber=@env.BUILD_NUMBER@ +buildTime=@env.BUILD_TIME@ diff --git a/analysis.gradle b/analysis.gradle new file mode 100644 index 000000000..2f9842eef --- /dev/null +++ b/analysis.gradle @@ -0,0 +1,16 @@ +//so that these can be conditionally enabled +apply plugin: 'java' +apply plugin: 'jacoco' +apply plugin: 'org.sonarqube' + +test { + jacoco { + includes['lavalink.*'] + } +} + +sonarqube { + properties { + property 'sonar.inclusions', 'src/main/java/lavalink/**/*' + } +} diff --git a/build.gradle b/build.gradle index a38fd05e4..2f42d7b18 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,20 @@ +buildscript { + ext { + springBootVersion = '2.0.2.RELEASE' + gradleGitVersion = '1.5.1' + sonarqubeVersion = '2.6.2' + } + repositories { + maven { url "https://plugins.gradle.org/m2/" } + maven { url 'http://repo.spring.io/plugins-release' } + } + dependencies { + classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:${gradleGitVersion}" + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:${sonarqubeVersion}" + } +} + allprojects { repositories { @@ -11,22 +28,12 @@ allprojects { } subprojects { - buildscript { - ext { - springBootVersion = '2.0.2.RELEASE' - gradleGitVersion = '1.4.21' - } - repositories { - maven { url "https://plugins.gradle.org/m2/" } - maven { url 'http://repo.spring.io/plugins-release' } - } - dependencies { - classpath "gradle.plugin.com.gorylenko.gradle-git-properties:gradle-git-properties:${gradleGitVersion}" - classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" - } - } apply plugin: 'java' apply plugin: 'idea' + if (project.hasProperty("includeAnalysis")) { + project.logger.lifecycle("applying analysis plugins") + apply from: "../analysis.gradle" + } sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -42,7 +49,7 @@ subprojects { ext { //@formatter:off - lavaplayerVersion = '1.2.64' + lavaplayerVersion = '1.3.7' jdaAudioVersion = '4d7abb48aec49f0a996ba0d87df34fdc67f71275' jdaNasVersion = '1.0.6.2-JDA-Audio' jappVersion = '1.2'