diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f33409c --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Discord Radio Bot + +> [!IMPORTANT] +> You are currently viewing the Java rewrite of the bot. I am not done jet and some features are missing. If you are looking for the old Node.js based version, click here. + +Plays radio streams directly inside your Discord server. +This bot has no commands, it's for playing radio streams only. +You can specify your own radio stream in the config. +
+ +Important: The provided url MUST be a link to a DIRECT MEDIA STREAM. This means https://radioXYZ.fm is not a valid url!
+Stream urls normally look like https://play.radioXYZ.fm/source.mp3 +
+ +## How to +### Requirements +You need this. + +1. Create a new Bot User + First you need to create a new bot account.
+ Head over to the Discord Developer Portal, create a new bot instance and get the bot token. +2. A working JRE setup + If you do not have one already, get it here: AdoptOpenJDK + +### Running the Bot +Download the latest release. + +Then you have to create a file called ``application.properties`` in the same directory as the bot. +Fill it like so: +```properties +discord.token=<> +discord.channel=<> +``` + +And run it: ``java -jar radiobot.jar`` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..427cfdf --- /dev/null +++ b/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + space.parzival.discord + radiobot + 0.0.1-SNAPSHOT + radiobot + Plays radio streams directly inside your Discord server. + + 17 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + net.dv8tion + JDA + 5.0.0-beta.12 + + + com.github.walkyst + lavaplayer-fork + 1.4.3 + + + org.apache.maven + maven-artifact + 3.9.4 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + build-info + + build-info + + + + + + + + + + jitpack + https://jitpack.io + + + + diff --git a/src/main/java/space/parzival/discord/radiobot/ClientConfiguration.java b/src/main/java/space/parzival/discord/radiobot/ClientConfiguration.java new file mode 100644 index 0000000..997da5d --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/ClientConfiguration.java @@ -0,0 +1,29 @@ +package space.parzival.discord.radiobot; + +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import space.parzival.discord.radiobot.properties.ClientProperties; + +import java.util.List; + +@Slf4j +@Component +public class ClientConfiguration { + + @Bean + public JDA discordInstance(ClientProperties clientProperties, List events) { + JDA client = JDABuilder + .createDefault(clientProperties.getToken()) + .build(); + + events.forEach(client::addEventListener); + + log.debug("New Discord instance created."); + return client; + } +} diff --git a/src/main/java/space/parzival/discord/radiobot/RadioBotApplication.java b/src/main/java/space/parzival/discord/radiobot/RadioBotApplication.java new file mode 100644 index 0000000..479399a --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/RadioBotApplication.java @@ -0,0 +1,25 @@ +package space.parzival.discord.radiobot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import space.parzival.discord.radiobot.properties.ClientProperties; +import space.parzival.discord.radiobot.properties.HttpProperties; +import space.parzival.discord.radiobot.properties.StreamProperties; + +@SpringBootApplication +@EnableConfigurationProperties +@Import({ + ClientProperties.class, + StreamProperties.class, + HttpProperties.class +}) +public class RadioBotApplication { + + public static void main(String[] args) { + SpringApplication.run(RadioBotApplication.class, args); + } + +} diff --git a/src/main/java/space/parzival/discord/radiobot/VersionCheck.java b/src/main/java/space/parzival/discord/radiobot/VersionCheck.java new file mode 100644 index 0000000..93d2c18 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/VersionCheck.java @@ -0,0 +1,59 @@ +package space.parzival.discord.radiobot; + +import lombok.extern.slf4j.Slf4j; +import org.apache.http.message.BasicHeader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; +import org.springframework.stereotype.Component; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import space.parzival.discord.radiobot.properties.HttpProperties; +import space.parzival.discord.radiobot.model.GitHubRelease; + +import java.util.List; + +@Slf4j +@Component +public class VersionCheck implements InitializingBean { + + @Autowired + private HttpProperties httpProperties; + + @Autowired + private BuildProperties buildProperties; + + @Override + public void afterPropertiesSet() throws Exception { + ComparableVersion latest = this.getLatestVersion(); + ComparableVersion current = new ComparableVersion(buildProperties.getVersion()); + + if (current.compareTo(latest) > 0) { + log.warn("------------------------------------------------------------------"); + log.warn("You are currently running version {}, but {} ", current, latest); + log.warn("is already available. Please consider updating."); + log.warn("------------------------------------------------------------------"); + } + } + + private ComparableVersion getLatestVersion() { + WebClient githubClient = WebClient.builder() + .defaultHeader("User-Agent", httpProperties.getUserAgent()) + .baseUrl("https://api.github.com") + .build(); + + Mono response = githubClient + .get() + .uri(httpProperties.getVersionUrl()) + .retrieve() + .bodyToMono(GitHubRelease[].class); + + GitHubRelease[] releases = response.block(); + if (releases != null && releases.length >= 1) + return new ComparableVersion(releases[0].tag_name); + + return new ComparableVersion("0.0.0"); + } + +} diff --git a/src/main/java/space/parzival/discord/radiobot/events/Ready.java b/src/main/java/space/parzival/discord/radiobot/events/Ready.java new file mode 100644 index 0000000..053edf7 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/events/Ready.java @@ -0,0 +1,80 @@ +package space.parzival.discord.radiobot.events; + +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.audio.AudioSendHandler; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel; +import net.dv8tion.jda.api.events.session.ReadyEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import space.parzival.discord.radiobot.icecast.IcyAudioLoadResultHandler; +import space.parzival.discord.radiobot.icecast.IcyAudioPlayerManager; +import space.parzival.discord.radiobot.player.AudioPlayerSendHandler; +import space.parzival.discord.radiobot.properties.ClientProperties; + +import java.nio.ByteBuffer; + +@Slf4j +@Component +public class Ready extends ListenerAdapter { + + @Autowired + private IcyAudioPlayerManager playerManager; + + @Autowired + private ClientProperties clientProperties; + + public void onReady(@NotNull ReadyEvent event) { + log.info("This bot is now ready and connected to {} guilds.", event.getGuildTotalCount()); + + JDA client = event.getJDA(); + VoiceChannel channel = client.getVoiceChannelById(clientProperties.getChannel()); + assert channel != null; + Guild guild = channel.getGuild(); + guild.getAudioManager().openAudioConnection(channel); + + AudioPlayer player = playerManager.createPlayer(); + guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player)); + + playerManager.loadIcyStream("https://play.sas-media.ru/play_256", new IcyAudioLoadResultHandler() { + @Override + public void metadataUpdated(String metadata) { + + } + + @Override + public void trackLoaded(AudioTrack audioTrack) { + log.info("Playing {} by {}", audioTrack.getInfo().title, audioTrack.getInfo().author); + player.playTrack(audioTrack); + } + + @Override + public void playlistLoaded(AudioPlaylist audioPlaylist) { + + } + + @Override + public void noMatches() { + log.error("No matches."); + } + + @Override + public void loadFailed(FriendlyException e) { + log.error("Load failed:", e); + } + }); + } +} diff --git a/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioLoadResultHandler.java b/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioLoadResultHandler.java new file mode 100644 index 0000000..2046258 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioLoadResultHandler.java @@ -0,0 +1,9 @@ +package space.parzival.discord.radiobot.icecast; + +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; + +public interface IcyAudioLoadResultHandler extends AudioLoadResultHandler { + + void metadataUpdated(String metadata); +} diff --git a/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioPlayerManager.java b/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioPlayerManager.java new file mode 100644 index 0000000..c4f3535 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/icecast/IcyAudioPlayerManager.java @@ -0,0 +1,26 @@ +package space.parzival.discord.radiobot.icecast; + +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager; +import org.springframework.stereotype.Component; + +import java.util.concurrent.CompletableFuture; + +@Component +public class IcyAudioPlayerManager extends DefaultAudioPlayerManager { + + public IcyAudioPlayerManager() { + super(); + + // we do not need support for YouTube, so only http sources + this.registerSourceManager(new HttpAudioSourceManager()); + } + + + public void loadIcyStream(final String url, final IcyAudioLoadResultHandler resultHandler) { + this.loadItem(url, resultHandler); + + // todo: implement logic for current track info + } +} diff --git a/src/main/java/space/parzival/discord/radiobot/model/GitHubRelease.java b/src/main/java/space/parzival/discord/radiobot/model/GitHubRelease.java new file mode 100644 index 0000000..c138510 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/model/GitHubRelease.java @@ -0,0 +1,5 @@ +package space.parzival.discord.radiobot.model; + +public class GitHubRelease { + public String tag_name; +} diff --git a/src/main/java/space/parzival/discord/radiobot/player/AudioPlayerSendHandler.java b/src/main/java/space/parzival/discord/radiobot/player/AudioPlayerSendHandler.java new file mode 100644 index 0000000..60ba44f --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/player/AudioPlayerSendHandler.java @@ -0,0 +1,35 @@ +package space.parzival.discord.radiobot.player; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; +import net.dv8tion.jda.api.audio.AudioSendHandler; + +import java.nio.ByteBuffer; + +/** + * Helper to allow AudioPlayers to stream to Discord. + */ +public class AudioPlayerSendHandler implements AudioSendHandler { + private final AudioPlayer audioPlayer; + private AudioFrame lastFrame; + + public AudioPlayerSendHandler(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + } + + @Override + public boolean canProvide() { + lastFrame = audioPlayer.provide(); + return lastFrame != null; + } + + @Override + public ByteBuffer provide20MsAudio() { + return ByteBuffer.wrap(lastFrame.getData()); + } + + @Override + public boolean isOpus() { + return true; + } +} diff --git a/src/main/java/space/parzival/discord/radiobot/properties/ClientProperties.java b/src/main/java/space/parzival/discord/radiobot/properties/ClientProperties.java new file mode 100644 index 0000000..04603f4 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/properties/ClientProperties.java @@ -0,0 +1,21 @@ +package space.parzival.discord.radiobot.properties; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter @Setter +@ConfigurationProperties(prefix = "discord") +public class ClientProperties { + + /** + * The Discord Bot Token + */ + private String token; + + /** + * The channel id where the bot is playing. + */ + private String channel; +} diff --git a/src/main/java/space/parzival/discord/radiobot/properties/HttpProperties.java b/src/main/java/space/parzival/discord/radiobot/properties/HttpProperties.java new file mode 100644 index 0000000..f2da7a0 --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/properties/HttpProperties.java @@ -0,0 +1,18 @@ +package space.parzival.discord.radiobot.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "http") +public class HttpProperties { + + /** + * Useragent used to identify on webservers. + */ + private String UserAgent = "Discord Radio Bot (https://github.com/parzival-space/discord-radio-bot)"; + + private String VersionUrl = "/repos/parzival-space/discord-radio-bot/releases"; +} diff --git a/src/main/java/space/parzival/discord/radiobot/properties/StreamProperties.java b/src/main/java/space/parzival/discord/radiobot/properties/StreamProperties.java new file mode 100644 index 0000000..ff5299f --- /dev/null +++ b/src/main/java/space/parzival/discord/radiobot/properties/StreamProperties.java @@ -0,0 +1,16 @@ +package space.parzival.discord.radiobot.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "stream") +public class StreamProperties { + + /** + * The url to the audio stream. + */ + private String url = ""; +} diff --git a/src/test/java/space/parzival/discord/radiobot/RadioBotApplicationTests.java b/src/test/java/space/parzival/discord/radiobot/RadioBotApplicationTests.java new file mode 100644 index 0000000..b130a6d --- /dev/null +++ b/src/test/java/space/parzival/discord/radiobot/RadioBotApplicationTests.java @@ -0,0 +1,13 @@ +package space.parzival.discord.radiobot; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RadioBotApplicationTests { + + @Test + void contextLoads() { + } + +}