From 2f9e50fd3ec15f9c0944ca13c86bc6f581613ed8 Mon Sep 17 00:00:00 2001 From: Julius de Jeu Date: Sun, 14 Oct 2018 14:16:14 +0200 Subject: [PATCH] Add begin of music bot features --- build.gradle | 4 + src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt | 36 ++- .../nl/voidcorp/dbot/commands/Commands.kt | 16 ++ .../kotlin/nl/voidcorp/dbot/commands/Music.kt | 205 ++++++++++++++++++ .../nl/voidcorp/dbot/music/TrackScheduler.kt | 109 ++++++++++ 5 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt create mode 100644 src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt create mode 100644 src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt diff --git a/build.gradle b/build.gradle index eac3587..193df7c 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,10 @@ dependencies { compile "ch.qos.logback:logback-classic:1.2.1" compile 'io.javalin:javalin:2.3.0' compile 'com.github.salomonbrys.kotson:kotson:2.5.0' + compile 'com.jagrosh:jda-utilities:2.1.4' + compile 'com.sedmelluq:lavaplayer:1.3.7' + compile 'com.sedmelluq:jda-nas:1.0.6' + } diff --git a/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt b/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt index 279231c..9a07a52 100644 --- a/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt +++ b/src/main/kotlin/nl/voidcorp/dbot/UnityBot.kt @@ -1,22 +1,37 @@ package nl.voidcorp.dbot +import com.jagrosh.jdautilities.command.CommandClientBuilder +import com.jagrosh.jdautilities.examples.command.PingCommand +import com.sedmelluq.discord.lavaplayer.jdaudp.NativeAudioSendFactory +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager +import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager import io.javalin.Javalin import io.javalin.json.FromJsonMapper import io.javalin.json.JavalinJson import io.javalin.json.ToJsonMapper import net.dv8tion.jda.core.EmbedBuilder +import net.dv8tion.jda.core.JDA import net.dv8tion.jda.core.JDABuilder -import net.dv8tion.jda.core.OnlineStatus import net.dv8tion.jda.core.entities.Game import net.dv8tion.jda.webhook.WebhookClient import net.dv8tion.jda.webhook.WebhookClientBuilder +import nl.voidcorp.dbot.commands.helloCommand +import nl.voidcorp.dbot.commands.initMusic import nl.voidcorp.dbot.storage.GitlabWebhook import org.slf4j.LoggerFactory import java.io.File import java.io.FileReader +val playerManager = DefaultAudioPlayerManager() + +val cb = CommandClientBuilder() + +val log = LoggerFactory.getLogger("UnityBot") + + fun main(args: Array) { - val log = LoggerFactory.getLogger("UnityBot") + + playerManager.registerSourceManager(YoutubeAudioSourceManager(true)) JavalinJson.apply { toJsonMapper = object : ToJsonMapper { @@ -68,6 +83,19 @@ fun main(args: Array) { ctx.status(200).result("OK") } - val bot = JDABuilder(args[0]).setStatus(OnlineStatus.ONLINE).setGame(Game.watching("fraud")).addEventListener(Events).build() + cb.setOwnerId("168743656738521088") + cb.setPrefix("!") + cb.setAlternativePrefix("+") + cb.addCommand(PingCommand()) + cb.addCommand(helloCommand) + cb.setGame(Game.watching("fraud (and a broken music bot)")) -} \ No newline at end of file + + initMusic() + + + val client = cb.build() + bot = JDABuilder(args[0]).addEventListener(client).addEventListener(nl.voidcorp.dbot.Events).setAudioSendFactory(NativeAudioSendFactory()).build() +} + +lateinit var bot: JDA diff --git a/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt b/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt new file mode 100644 index 0000000..a3ada11 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/dbot/commands/Commands.kt @@ -0,0 +1,16 @@ +package nl.voidcorp.dbot.commands + +import com.jagrosh.jdautilities.command.CommandBuilder +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist +import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import net.dv8tion.jda.core.MessageBuilder +import nl.voidcorp.dbot.playerManager + +val helloCommand = CommandBuilder().setName("hello").setHelp("Say hello to Andy!") + .setGuildOnly(false) + .build { event -> + event.reply(MessageBuilder("Hello, ").append(event.author).build()) + } + diff --git a/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt b/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt new file mode 100644 index 0000000..49d3609 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/dbot/commands/Music.kt @@ -0,0 +1,205 @@ +package nl.voidcorp.dbot.commands + +import com.jagrosh.jdautilities.command.CommandBuilder +import com.jagrosh.jdautilities.command.CommandEvent +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer +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 com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame +import net.dv8tion.jda.core.EmbedBuilder +import net.dv8tion.jda.core.audio.AudioSendHandler +import net.dv8tion.jda.core.entities.Member +import nl.voidcorp.dbot.cb +import nl.voidcorp.dbot.log +import nl.voidcorp.dbot.music.TrackScheduler +import nl.voidcorp.dbot.playerManager +import java.time.LocalDateTime + + +val guildMusicMap = mutableMapOf() + +fun initMusic() { + AudioSourceManagers.registerRemoteSources(playerManager) + + + val queueCommand = CommandBuilder().setName("queue").addAlias("q").setHelp("Queue's a song").build { event -> + + + val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { + val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } + if (channel == null) { + event.reply("Join a voice Channel please!") + return@build + } + val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) + guildMusicMap[event.guild.idLong] = s + s + } + + + + playerManager.loadItem(event.args, object : AudioLoadResultHandler { + override fun loadFailed(exception: FriendlyException) { + event.reply("Shit's fucked!") + } + + override fun trackLoaded(track: AudioTrack) { + scheduler.queue(track, event.member) + } + + override fun noMatches() { + getLinkFromSearch(event, scheduler) + } + + override fun playlistLoaded(playlist: AudioPlaylist) { + for (t in playlist.tracks) { + scheduler.queue(t, event.member) + } + + + } + + }) + } + + val playCommand = CommandBuilder().setName("play").addAlias("p").setHelp("Plays the song instantly").build { event -> + val scheduler = if (guildMusicMap.containsKey(event.guild.idLong)) guildMusicMap[event.guild.idLong]!! else { + val channel = event.guild.voiceChannels.firstOrNull { it.members.contains(event.member) } + if (channel == null) { + event.reply("Join a voice Channel please!") + return@build + } + val s = TrackScheduler(playerManager.createPlayer(), event.guild, channel) + guildMusicMap[event.guild.idLong] = s + s + } + + playerManager.loadItem(event.args, object : AudioLoadResultHandler { + override fun loadFailed(exception: FriendlyException) { + event.reply("Shit's fucked!") + } + + override fun trackLoaded(track: AudioTrack) { + scheduler.insertFront(track, event.member) + } + + override fun noMatches() { + getLinkFromSearch(event, scheduler) + } + + override fun playlistLoaded(playlist: AudioPlaylist) { + for (t in playlist.tracks) { + scheduler.insertFront(t, event.member) + } + + + } + + }) + } + + val skipCommand = CommandBuilder().setName("skip").addAlias("s").setHelp("Skips the currently playing track").build { event -> + val scheduler = guildMusicMap[event.guild.idLong] + if (scheduler == null) { + event.reply("There is no music playing?") + } else { + scheduler.skip() + } + + } + + + cb.addCommands(playCommand, skipCommand, queueCommand) +} + +fun getLinkFromSearch(event: CommandEvent, scheduler: TrackScheduler) { + log.info("Searching for '${event.args}'") + /*val search = yt.search().list("id,snippet") + search.key = "AIzaSyDiEYrQNT-eDRZKy6JvQHBDymttwaLd7Mg" + search.q = event.args + search.type = "video" + search.fields = "items(id/kind,id/videoId,snippet/title,snippet/thumbnails/high/url,snippet/description,snippet/channelTitle)" + search.maxResults = 10 + val res = search.execute() + if (res.isEmpty()) { + event.reply("Really nothing was found...") + } else { + var found = false + for (r in res.items) { + if (r.id.kind == "youtube#video" && !found) { + found = true + val id = r.id.videoId + + } + } + if (!found) { + event.replyError("I could really not find anything for '${event.args}'") + + } + }*/ + + playerManager.loadItem("ytsearch:${event.args}", object : AudioLoadResultHandler { + override fun loadFailed(exception: FriendlyException) { + event.reply("Shit's fucked!") + } + + override fun trackLoaded(track: AudioTrack) { + scheduler.queue(track, event.member) + /*event.reply( + EmbedBuilder().setImage(r.snippet.thumbnails.high.url).setTitle(r.snippet.title, "https://youtu.be/$id") + .setAuthor(r.snippet.channelTitle).setFooter("Requested by ${event.member.effectiveName}", event.author.effectiveAvatarUrl) + .setDescription(r.snippet.description).build() + )*/ + + } + + override fun noMatches() { + + } + + override fun playlistLoaded(playlist: AudioPlaylist) { + if (!playlist.isSearchResult) + for (t in playlist.tracks) { + scheduler.queue(t, event.member) + } + else { + val track = playlist.tracks.first() + + event.reply(EmbedBuilder() + .setFooter("Requested by ${event.member.effectiveName}", event.member.user.effectiveAvatarUrl) + .setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setThumbnail("https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg\n") + .setTimestamp(LocalDateTime.now()).build()) + + scheduler.queue(track, event.member) + + + + } + + + } + + }) + +} + +class AudioPlayerSendHandler(private val audioPlayer: AudioPlayer) : AudioSendHandler { + private var lastFrame: AudioFrame? = null + + override fun canProvide(): Boolean { + lastFrame = audioPlayer.provide() + return lastFrame != null + } + + override fun provide20MsAudio(): ByteArray { + return lastFrame!!.data + } + + override fun isOpus(): Boolean { + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt b/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt new file mode 100644 index 0000000..4a66b48 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/dbot/music/TrackScheduler.kt @@ -0,0 +1,109 @@ +package nl.voidcorp.dbot.music + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter +import com.sedmelluq.discord.lavaplayer.track.AudioTrack +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason +import net.dv8tion.jda.core.EmbedBuilder +import net.dv8tion.jda.core.MessageBuilder +import net.dv8tion.jda.core.entities.Guild +import net.dv8tion.jda.core.entities.Member +import net.dv8tion.jda.core.entities.TextChannel +import net.dv8tion.jda.core.entities.VoiceChannel +import nl.voidcorp.dbot.commands.AudioPlayerSendHandler +import nl.voidcorp.dbot.commands.guildMusicMap +import nl.voidcorp.dbot.log +import java.time.LocalDateTime +import java.util.* +import kotlin.concurrent.thread + + +class TrackScheduler(val player: AudioPlayer, val guild: Guild, channel: VoiceChannel) : AudioEventAdapter() { + val musicChannel: TextChannel + init { + player.addListener(this) + + val audio = guild.audioManager + + audio.sendingHandler = AudioPlayerSendHandler(player) + audio.openAudioConnection(channel) + + musicChannel = guild.getTextChannelsByName("music", true).firstOrNull() ?: (guild.getTextChannelsByName("general", true).firstOrNull() + ?: guild.defaultChannel!!) + + } + + private val q = ArrayDeque() + /*override fun onEvent(event: AudioEvent) { + if (event is TrackEndEvent) { + + } + }*/ + + override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, endReason: AudioTrackEndReason) { + if (q.isNotEmpty()) { + player.playTrack(q.remove()) + } else { + stopPlay(endReason) + player.destroy() + } + } + + override fun onTrackStart(player: AudioPlayer, track: AudioTrack) { + if (track.userData is Member) + musicChannel.sendMessage(EmbedBuilder() + .setFooter("Requested by ${(track.userData as Member).effectiveName}", (track.userData as Member) + .user.effectiveAvatarUrl).setAuthor(track.info.author).setTitle(track.info.title, track.info.uri) + .setThumbnail("https://img.youtube.com/vi/${track.info.identifier}/hqdefault.jpg\n") + .setTimestamp(LocalDateTime.now()).build()).append("Now playing").queue() + + + } + + fun queue(track: AudioTrack, member: Member) { + track.userData = member + if (q.isEmpty() && player.playingTrack == null) { + log.info("Queue is empty, playing a track") + player.playTrack(track) + } else { + log.info("Track Queue'd") + q.addLast(track) + } + } + + fun insertFront(track: AudioTrack, member: Member) { + track.userData = member + if (q.isEmpty() && player.playingTrack == null) { + log.info("Queue is empty, playing a track") + player.playTrack(track) + } else { + log.info("Track inserted") + q.addFirst(track) + } + } + + + fun skip() { + if (q.isNotEmpty()) { + log.info("skipped Track!") + player.playTrack(q.remove()) + } else { + stopPlay(AudioTrackEndReason.REPLACED) + player.destroy() + } + } + + fun isQueueEmpty() = q.isEmpty() + + + fun stopPlay(endReason: AudioTrackEndReason) { + + if (endReason != AudioTrackEndReason.REPLACED) { + guildMusicMap.remove(guild.idLong) + musicChannel.sendMessage("Playlist has ended, leaving the voice channel!").queue() + thread { guild.audioManager.closeAudioConnection() } + } + } + +} +