feat: added basic music controls, added music, added gitlab-ci.yml updated README.md
This commit is contained in:
parent
a9c4fe5da3
commit
8280620091
18 changed files with 553 additions and 73 deletions
34
.gitlab-ci.yml
Normal file
34
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
#image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/gradle
|
||||||
|
image: gradle
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- publish
|
||||||
|
|
||||||
|
variables:
|
||||||
|
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
|
||||||
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- export GRADLE_USER_HOME=`pwd`/.gradle
|
||||||
|
- rm -f .gradle/caches/modules-2/modules-2.lock
|
||||||
|
- rm -fr .gradle/caches/*/plugin-resolution/
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .gradle/wrapper
|
||||||
|
- .gradle/caches
|
||||||
|
- build
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- gradle shadowJar
|
||||||
|
except:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
publish:
|
||||||
|
stage: publish
|
||||||
|
script:
|
||||||
|
- gradle publish
|
||||||
|
|
46
README.md
46
README.md
|
@ -1,2 +1,46 @@
|
||||||
# Botendo
|
# Botendo version 5
|
||||||
|
|
||||||
|
"5th times the charm" ~ me
|
||||||
|
|
||||||
A Discord music bot, written in Kotlin using the kord library.
|
A Discord music bot, written in Kotlin using the kord library.
|
||||||
|
<div class="aside">
|
||||||
|
<img src="https://img.shields.io/badge/100%25-Selfmade-success" alt="100% Selfmade"/>
|
||||||
|
<img src="https://img.shields.io/badge/0%25-optimized-orange" alt="0% optimized"/>
|
||||||
|
<img src="https://img.shields.io/badge/fuck%20it-ship%20it-orange" alt="fuck it, ship it"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<div class="aside">
|
||||||
|
<img src="https://img.shields.io/badge/limited__dev-Owner%20%26%20Developer-blue" alt="limited_dev: Owner & Developer"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- info -- Show basic infos about the bot
|
||||||
|
- play -- Play a song
|
||||||
|
- stop -- Stop playing a song and leave the vc
|
||||||
|
|
||||||
|
## How to self-host
|
||||||
|
|
||||||
|
1. Download the latest release from the Package Registry ("Packages and registries" > "Package Registry")
|
||||||
|
1. It should be called something like this: "Botendo-X.X.X-xxxxxxxx-prod.jar" (replace "X.X.X" with the latest
|
||||||
|
version and xxxxxxxx" with the commit its based on.)
|
||||||
|
2. If you want to run an early version, which may be (very) unsable, you can run a development version. Just use an
|
||||||
|
entry ending in "-dev.jar"
|
||||||
|
2. Place it anywhere you want.
|
||||||
|
3. Run the following command:
|
||||||
|
> java -jar Botendo-X.X.X-xxxxxxxx-prod.jar
|
||||||
|
4. The bot should start and create a config file named "credentials.nils"
|
||||||
|
5. Open it and put in your credentials.
|
||||||
|
1. token: your Discord bot token
|
||||||
|
2. lavaip: the IP of your LavaLink instance e.g.: ("ws://192.168.178.1:2333")
|
||||||
|
3. lavapw: your password for the LavaLink instance
|
||||||
|
6. Rerun the command
|
||||||
|
> java -jar Botendo-X.X.X-xxxxxxxx-prod.jar
|
||||||
|
7. The bot should now be up and running.
|
||||||
|
|
||||||
|
## How to set up workspace
|
||||||
|
|
||||||
|
Install IntellJ and import the project from git.
|
||||||
|
Done.
|
||||||
|
|
|
@ -34,6 +34,7 @@ group = "de.limited_dev.botendo"
|
||||||
version = System.getenv("CI_COMMIT_TAG")?.let { "$it-${System.getenv("CI_COMMIT_SHORT_SHA")}-prod" }
|
version = System.getenv("CI_COMMIT_TAG")?.let { "$it-${System.getenv("CI_COMMIT_SHORT_SHA")}-prod" }
|
||||||
?: System.getenv("CI_COMMIT_SHORT_SHA")?.let { "$it-dev" }
|
?: System.getenv("CI_COMMIT_SHORT_SHA")?.let { "$it-dev" }
|
||||||
?: "DevelopmentBuild"
|
?: "DevelopmentBuild"
|
||||||
|
|
||||||
val kordver = "0.8.1"
|
val kordver = "0.8.1"
|
||||||
val lavaver = "3.8.0"
|
val lavaver = "3.8.0"
|
||||||
val coroutinesver = "1.1.0"
|
val coroutinesver = "1.1.0"
|
||||||
|
|
|
@ -22,9 +22,9 @@ package de.limited_dev.botendo
|
||||||
import de.limited_dev.botendo.commands.slash.component.SlashCommandManager
|
import de.limited_dev.botendo.commands.slash.component.SlashCommandManager
|
||||||
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
import de.limited_dev.botendo.commands.slash.component.options.OptionType
|
import de.limited_dev.botendo.commands.slash.component.options.OptionType
|
||||||
|
import de.limited_dev.botendo.util.CredentialManager
|
||||||
import de.limited_dev.botendo.util.Logger
|
import de.limited_dev.botendo.util.Logger
|
||||||
import de.limited_dev.botendo.util.MessageUtil
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
import de.limited_dev.botendo.util.TokenManager
|
|
||||||
import dev.kord.core.Kord
|
import dev.kord.core.Kord
|
||||||
import dev.kord.core.event.interaction.GuildChatInputCommandInteractionCreateEvent
|
import dev.kord.core.event.interaction.GuildChatInputCommandInteractionCreateEvent
|
||||||
import dev.kord.core.on
|
import dev.kord.core.on
|
||||||
|
@ -44,10 +44,10 @@ object Bot {
|
||||||
Logger.out("Starting Bot...")
|
Logger.out("Starting Bot...")
|
||||||
|
|
||||||
//Load config
|
//Load config
|
||||||
TokenManager.load()
|
CredentialManager.load()
|
||||||
|
|
||||||
//Don't run the bot when there is no bot token in config
|
//Don't run the bot when there is no bot token in config
|
||||||
if (TokenManager.token == "empty") {
|
if (CredentialManager.token == "empty") {
|
||||||
Logger.out("The config does not contain a bot token")
|
Logger.out("The config does not contain a bot token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -56,13 +56,13 @@ object Bot {
|
||||||
SlashCommandManager.register()
|
SlashCommandManager.register()
|
||||||
|
|
||||||
//Add Bot token to kord
|
//Add Bot token to kord
|
||||||
kord = Kord(TokenManager.token)
|
kord = Kord(CredentialManager.token)
|
||||||
|
|
||||||
//Register Command Listener
|
//Register Command Listener
|
||||||
kord.on<GuildChatInputCommandInteractionCreateEvent> {
|
kord.on<GuildChatInputCommandInteractionCreateEvent> {
|
||||||
val response = interaction.deferPublicResponse()
|
val response = interaction.deferPublicResponse()
|
||||||
val command = interaction.command
|
val command = interaction.command
|
||||||
Logger.out("Command /${command.rootName}")
|
Logger.out("Command /${command.rootName} with ${command.options.size} Option${if (command.options.size == 1) "" else "s"}")
|
||||||
for (c in SlashCommandManager.commands) {
|
for (c in SlashCommandManager.commands) {
|
||||||
if (c.name != command.rootName)
|
if (c.name != command.rootName)
|
||||||
continue
|
continue
|
||||||
|
@ -128,10 +128,8 @@ object Bot {
|
||||||
//Add LavaLink
|
//Add LavaLink
|
||||||
lava = kord.lavakord()
|
lava = kord.lavakord()
|
||||||
|
|
||||||
//Add the LavaLink node
|
//Add the LavaLink node from config
|
||||||
lava.addNode("ws://192.168.178.2:2333", "youshallnotpass")
|
lava.addNode(CredentialManager.linkip, CredentialManager.linkpw)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Logger.out("Logging in")
|
Logger.out("Logging in")
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,7 @@
|
||||||
|
|
||||||
package de.limited_dev.botendo.commands.slash.component
|
package de.limited_dev.botendo.commands.slash.component
|
||||||
|
|
||||||
import de.limited_dev.botendo.commands.slash.music.PlayCommand
|
import de.limited_dev.botendo.commands.slash.music.*
|
||||||
import de.limited_dev.botendo.commands.slash.music.StopCommand
|
|
||||||
import de.limited_dev.botendo.commands.slash.util.InfoCommand
|
import de.limited_dev.botendo.commands.slash.util.InfoCommand
|
||||||
|
|
||||||
object SlashCommandManager {
|
object SlashCommandManager {
|
||||||
|
@ -31,5 +30,8 @@ object SlashCommandManager {
|
||||||
commands.add(InfoCommand())
|
commands.add(InfoCommand())
|
||||||
commands.add(PlayCommand())
|
commands.add(PlayCommand())
|
||||||
commands.add(StopCommand())
|
commands.add(StopCommand())
|
||||||
|
commands.add(NowPlayingCommand())
|
||||||
|
commands.add(QueueCommand())
|
||||||
|
commands.add(SkipCommand())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,4 @@
|
||||||
|
|
||||||
package de.limited_dev.botendo.commands.slash.component.options
|
package de.limited_dev.botendo.commands.slash.component.options
|
||||||
|
|
||||||
data class CommandOption(val name: String, val description: String, val required: Boolean, val type: OptionType) {
|
data class CommandOption(val name: String, val description: String, val required: Boolean, val type: OptionType)
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Botendo
|
||||||
|
* Copyright (C) 2023 limited_dev
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.limited_dev.botendo.commands.slash.music
|
||||||
|
|
||||||
|
import de.limited_dev.botendo.Bot
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
|
import de.limited_dev.botendo.util.TimeUtil
|
||||||
|
import de.limited_dev.botendo.util.UrlUtil
|
||||||
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
|
import dev.schlaubi.lavakord.audio.Link
|
||||||
|
|
||||||
|
class NowPlayingCommand : SlashCommand("nowplaying", "Show what's currently playing", null) {
|
||||||
|
override suspend fun onSlashCommand(
|
||||||
|
interaction: GuildChatInputCommandInteraction,
|
||||||
|
response: DeferredPublicMessageInteractionResponseBehavior,
|
||||||
|
args: Map<CommandOption, String>
|
||||||
|
) {
|
||||||
|
val guildId = interaction.guildId
|
||||||
|
val link = Bot.lava.getLink(guildId.toString())
|
||||||
|
val player = link.player
|
||||||
|
if (link.state != Link.State.CONNECTED) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not connected",
|
||||||
|
"I'm not in a VC and therefore, I am not playing anything."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val track = player.playingTrack
|
||||||
|
if (track == null) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not Playing",
|
||||||
|
"I'm not playing anything currently"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MessageUtil.sendEmbedForInteractionWithImage(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Currently playing",
|
||||||
|
"**${track.title}**\n*Now Playing*\nby ${track.author} :: ${
|
||||||
|
TimeUtil.getTimeFormatedRaw(
|
||||||
|
track.length.inWholeMilliseconds
|
||||||
|
)
|
||||||
|
}\n" +
|
||||||
|
">>>${track.uri}",
|
||||||
|
"https://img.youtube.com/vi/" + UrlUtil.getYtThumbnailUrl(track.uri!!) + "/maxresdefault.jpg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,10 +19,11 @@
|
||||||
|
|
||||||
package de.limited_dev.botendo.commands.slash.music
|
package de.limited_dev.botendo.commands.slash.music
|
||||||
|
|
||||||
import de.limited_dev.botendo.Bot
|
import de.limited_dev.botendo.Bot.lava
|
||||||
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
||||||
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
import de.limited_dev.botendo.commands.slash.component.options.OptionType
|
import de.limited_dev.botendo.commands.slash.component.options.OptionType
|
||||||
|
import de.limited_dev.botendo.commands.slash.music.component.MusicManager
|
||||||
import de.limited_dev.botendo.util.MessageUtil
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
|
@ -39,7 +40,7 @@ class PlayCommand : SlashCommand(
|
||||||
args: Map<CommandOption, String>
|
args: Map<CommandOption, String>
|
||||||
) {
|
) {
|
||||||
val guildId = interaction.guildId
|
val guildId = interaction.guildId
|
||||||
val link = Bot.lava.getLink(guildId.toString())
|
val link = lava.getLink(guildId.toString())
|
||||||
|
|
||||||
val voiceState = interaction.user.getVoiceStateOrNull()
|
val voiceState = interaction.user.getVoiceStateOrNull()
|
||||||
if (voiceState == null) {
|
if (voiceState == null) {
|
||||||
|
@ -56,6 +57,14 @@ class PlayCommand : SlashCommand(
|
||||||
|
|
||||||
if (link.state != Link.State.CONNECTED) {
|
if (link.state != Link.State.CONNECTED) {
|
||||||
link.connectAudio(channelId!!.value)
|
link.connectAudio(channelId!!.value)
|
||||||
|
} else if (link.state == Link.State.CONNECTED && link.lastChannelId != channelId!!.value) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"You are not in my VC",
|
||||||
|
"We are not in the same VC and therefore, you cannot play any music"
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val query = args[this.options!![0]]
|
val query = args[this.options!![0]]
|
||||||
|
@ -64,5 +73,7 @@ class PlayCommand : SlashCommand(
|
||||||
} else {
|
} else {
|
||||||
"ytsearch:$query"
|
"ytsearch:$query"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MusicManager.addToQueue(interaction, response, link, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Botendo
|
||||||
|
* Copyright (C) 2023 limited_dev
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.limited_dev.botendo.commands.slash.music
|
||||||
|
|
||||||
|
import de.limited_dev.botendo.Bot
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
|
import de.limited_dev.botendo.commands.slash.music.component.MusicManager
|
||||||
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
|
import de.limited_dev.botendo.util.TimeUtil
|
||||||
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
|
import dev.schlaubi.lavakord.audio.Link
|
||||||
|
|
||||||
|
class QueueCommand : SlashCommand("queue", "Show whats up next", null) {
|
||||||
|
override suspend fun onSlashCommand(
|
||||||
|
interaction: GuildChatInputCommandInteraction,
|
||||||
|
response: DeferredPublicMessageInteractionResponseBehavior,
|
||||||
|
args: Map<CommandOption, String>
|
||||||
|
) {
|
||||||
|
val guildId = interaction.guildId
|
||||||
|
val link = Bot.lava.getLink(guildId.toString())
|
||||||
|
val player = link.player
|
||||||
|
if (link.state != Link.State.CONNECTED) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not connected",
|
||||||
|
"I'm not in a VC and therefore, I am not playing anything."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val track = player.playingTrack
|
||||||
|
if (track == null) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Queue empty",
|
||||||
|
"I'm not playing anything currently"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val gts = MusicManager.getGuildTrackScheduler(interaction.guild.asGuild(), player)
|
||||||
|
val q = gts.getQueue()
|
||||||
|
var desc =
|
||||||
|
"""${"**" + track.title + " - " + TimeUtil.getTimeFormatedShortend(track.length.inWholeMilliseconds)} (${track.author})**""" + "\n"
|
||||||
|
for ((i, tr) in q.withIndex()) {
|
||||||
|
if (i >= 14)
|
||||||
|
continue
|
||||||
|
desc += tr.info.title + " - " + TimeUtil.getTimeFormatedShortend(tr.info.length) + " (" + tr.info.author + ")\n"
|
||||||
|
}
|
||||||
|
MessageUtil.sendEmbedForInteraction(interaction, response, "Queue", desc)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Botendo
|
||||||
|
* Copyright (C) 2023 limited_dev
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.limited_dev.botendo.commands.slash.music
|
||||||
|
|
||||||
|
import de.limited_dev.botendo.Bot
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
||||||
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
|
import de.limited_dev.botendo.commands.slash.music.component.MusicManager
|
||||||
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
|
import de.limited_dev.botendo.util.TimeUtil
|
||||||
|
import de.limited_dev.botendo.util.UrlUtil
|
||||||
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
|
import dev.schlaubi.lavakord.audio.Link
|
||||||
|
|
||||||
|
class SkipCommand : SlashCommand("skip", "Skip to the next song in queue", null) {
|
||||||
|
override suspend fun onSlashCommand(
|
||||||
|
interaction: GuildChatInputCommandInteraction,
|
||||||
|
response: DeferredPublicMessageInteractionResponseBehavior,
|
||||||
|
args: Map<CommandOption, String>
|
||||||
|
) {
|
||||||
|
val guildId = interaction.guildId
|
||||||
|
val link = Bot.lava.getLink(guildId.toString())
|
||||||
|
val player = link.player
|
||||||
|
val voiceState = interaction.user.getVoiceStateOrNull()
|
||||||
|
if (voiceState == null) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"You are not connected to a VC",
|
||||||
|
"Please connect to a VC"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val channelId = voiceState.channelId
|
||||||
|
if (link.state != Link.State.CONNECTED) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not connected",
|
||||||
|
"I'm not in a VC and therefore, I am not playing anything."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} else if (link.state == Link.State.CONNECTED && link.lastChannelId != channelId!!.value) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"You are not in my VC",
|
||||||
|
"We are not in the same VC and therefore, you cannot control the music"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var track = player.playingTrack
|
||||||
|
if (track == null) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not playing",
|
||||||
|
"I'm not playing anything currently"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val gts = MusicManager.getGuildTrackScheduler(interaction.guild.asGuild(), player)
|
||||||
|
track = gts.getHead().toTrack()
|
||||||
|
gts.playNext()
|
||||||
|
MessageUtil.sendEmbedForInteractionWithImage(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Skipped song; now playing",
|
||||||
|
"**${track.title}**\n*Now Playing*\nby ${track.author} :: ${
|
||||||
|
TimeUtil.getTimeFormatedRaw(
|
||||||
|
track.length.inWholeMilliseconds
|
||||||
|
)
|
||||||
|
}\n" +
|
||||||
|
">>>${track.uri}",
|
||||||
|
"https://img.youtube.com/vi/" + UrlUtil.getYtThumbnailUrl(track.uri!!) + "/maxresdefault.jpg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,9 +22,11 @@ package de.limited_dev.botendo.commands.slash.music
|
||||||
import de.limited_dev.botendo.Bot
|
import de.limited_dev.botendo.Bot
|
||||||
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
import de.limited_dev.botendo.commands.slash.component.SlashCommand
|
||||||
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
import de.limited_dev.botendo.commands.slash.component.options.CommandOption
|
||||||
|
import de.limited_dev.botendo.commands.slash.music.component.MusicManager
|
||||||
import de.limited_dev.botendo.util.MessageUtil
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
|
import dev.schlaubi.lavakord.audio.Link
|
||||||
|
|
||||||
class StopCommand : SlashCommand("stop", "Stop playing and start leavin'", null) {
|
class StopCommand : SlashCommand("stop", "Stop playing and start leavin'", null) {
|
||||||
override suspend fun onSlashCommand(
|
override suspend fun onSlashCommand(
|
||||||
|
@ -35,8 +37,39 @@ class StopCommand : SlashCommand("stop", "Stop playing and start leavin'", null)
|
||||||
val guildId = interaction.guildId
|
val guildId = interaction.guildId
|
||||||
val link = Bot.lava.getLink(guildId.toString())
|
val link = Bot.lava.getLink(guildId.toString())
|
||||||
val player = link.player
|
val player = link.player
|
||||||
|
|
||||||
|
val voiceState = interaction.user.getVoiceStateOrNull()
|
||||||
|
if (voiceState == null) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"You are not connected to a VC",
|
||||||
|
"Please connect to my VC"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val channelId = voiceState.channelId
|
||||||
|
if (link.state != Link.State.CONNECTED) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"Not connected",
|
||||||
|
"I'm not in a VC and therefore, I am not playing anything."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
} else if (link.state == Link.State.CONNECTED && link.lastChannelId != channelId!!.value) {
|
||||||
|
MessageUtil.sendEmbedForInteraction(
|
||||||
|
interaction,
|
||||||
|
response,
|
||||||
|
"You are not in my VC",
|
||||||
|
"We are not in the same VC"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
player.stopTrack()
|
player.stopTrack()
|
||||||
link.destroy()
|
link.destroy()
|
||||||
|
MusicManager.getGuildTrackScheduler(interaction.getGuild(), player).clear()
|
||||||
MessageUtil.sendEmbedForInteraction(interaction, response, "I stopped and left", "just like your girlfriend")
|
MessageUtil.sendEmbedForInteraction(interaction, response, "I stopped and left", "just like your girlfriend")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Botendo
|
||||||
|
* Copyright (C) 2023 limited_dev
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package de.limited_dev.botendo.commands.slash.music.component
|
||||||
|
|
||||||
|
import de.limited_dev.botendo.util.Logger
|
||||||
|
import dev.schlaubi.lavakord.audio.TrackEndEvent
|
||||||
|
import dev.schlaubi.lavakord.audio.TrackExceptionEvent
|
||||||
|
import dev.schlaubi.lavakord.audio.TrackStuckEvent
|
||||||
|
import dev.schlaubi.lavakord.audio.on
|
||||||
|
import dev.schlaubi.lavakord.audio.player.Player
|
||||||
|
import dev.schlaubi.lavakord.rest.TrackResponse
|
||||||
|
import java.util.concurrent.BlockingQueue
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
|
||||||
|
class GuildTrackScheduler(val pl: Player) {
|
||||||
|
|
||||||
|
private var queue: BlockingQueue<TrackResponse.PartialTrack> = LinkedBlockingQueue()
|
||||||
|
var repeating = false
|
||||||
|
private var hasRegisteredEvents = false
|
||||||
|
|
||||||
|
///Add a track to queue and start playing, if there is no song currently playing
|
||||||
|
suspend fun queue(track: TrackResponse.PartialTrack) {
|
||||||
|
if (this.pl.playingTrack == null) {
|
||||||
|
play(track)
|
||||||
|
} else {
|
||||||
|
queue.offer(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun playNext() {
|
||||||
|
if (!queue.isEmpty())
|
||||||
|
this.pl.playTrack(queue.poll())
|
||||||
|
else
|
||||||
|
this.pl.stopTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun play(tr: TrackResponse.PartialTrack) {
|
||||||
|
this.pl.playTrack(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEvents() {
|
||||||
|
if (hasRegisteredEvents)
|
||||||
|
return
|
||||||
|
hasRegisteredEvents = true
|
||||||
|
|
||||||
|
Logger.out("Adding track events to GuildTrackScheduler...")
|
||||||
|
|
||||||
|
pl.on<TrackEndEvent> {
|
||||||
|
onTrackEnd(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
pl.on<TrackStuckEvent> {
|
||||||
|
onTrackStuck(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
pl.on<TrackExceptionEvent> {
|
||||||
|
onTrackExc(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onTrackEnd(e: TrackEndEvent) {
|
||||||
|
if (e.reason.mayStartNext) {
|
||||||
|
if (repeating) {
|
||||||
|
this.pl.playTrack(e.track.copy())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Logger.out("Track has ended; Playing next...")
|
||||||
|
playNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onTrackStuck(e: TrackStuckEvent) {
|
||||||
|
Logger.out("Track is stuck, retrying...")
|
||||||
|
this.pl.playTrack(e.track.copy())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onTrackExc(e: TrackExceptionEvent) {
|
||||||
|
Logger.out("Track had an exception, retrying...")
|
||||||
|
this.pl.playTrack(e.track.copy())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
Logger.out("Clearing queue...")
|
||||||
|
repeating = false
|
||||||
|
queue.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getQueue(): List<TrackResponse.PartialTrack> {
|
||||||
|
return queue.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHead(): TrackResponse.PartialTrack {
|
||||||
|
return queue.first()
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,20 +21,25 @@ package de.limited_dev.botendo.commands.slash.music.component
|
||||||
|
|
||||||
import de.limited_dev.botendo.util.MessageUtil
|
import de.limited_dev.botendo.util.MessageUtil
|
||||||
import de.limited_dev.botendo.util.TimeUtil
|
import de.limited_dev.botendo.util.TimeUtil
|
||||||
|
import de.limited_dev.botendo.util.UrlUtil
|
||||||
|
import dev.kord.common.entity.Snowflake
|
||||||
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
import dev.kord.core.behavior.interaction.response.DeferredPublicMessageInteractionResponseBehavior
|
||||||
|
import dev.kord.core.entity.Guild
|
||||||
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction
|
||||||
import dev.schlaubi.lavakord.audio.Link
|
import dev.schlaubi.lavakord.audio.Link
|
||||||
import dev.schlaubi.lavakord.audio.TrackEndEvent
|
import dev.schlaubi.lavakord.audio.player.Player
|
||||||
import dev.schlaubi.lavakord.audio.on
|
|
||||||
import dev.schlaubi.lavakord.rest.TrackResponse
|
import dev.schlaubi.lavakord.rest.TrackResponse
|
||||||
import dev.schlaubi.lavakord.rest.loadItem
|
import dev.schlaubi.lavakord.rest.loadItem
|
||||||
|
|
||||||
object MusicManager {
|
object MusicManager {
|
||||||
private var musicManagerMap: MutableMap<Long, GuildMusicManager>? = null
|
private var musicManagerMap: MutableMap<Snowflake, GuildTrackScheduler> = mutableMapOf()
|
||||||
|
|
||||||
init {
|
fun getGuildTrackScheduler(guild: Guild, player: Player): GuildTrackScheduler {
|
||||||
musicManagerMap = HashMap()
|
return musicManagerMap.computeIfAbsent(guild.id) {
|
||||||
|
GuildTrackScheduler(player)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun addToQueue(
|
suspend fun addToQueue(
|
||||||
interaction: GuildChatInputCommandInteraction,
|
interaction: GuildChatInputCommandInteraction,
|
||||||
|
@ -54,60 +59,68 @@ object MusicManager {
|
||||||
) {
|
) {
|
||||||
val player = link.player
|
val player = link.player
|
||||||
val item = link.loadItem(search)
|
val item = link.loadItem(search)
|
||||||
|
val gts = getGuildTrackScheduler(interaction.getGuild(), player)
|
||||||
|
|
||||||
player.on<TrackEndEvent> {
|
gts.addEvents()
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
when (item.loadType) {
|
when (item.loadType) {
|
||||||
TrackResponse.LoadType.TRACK_LOADED -> {
|
TrackResponse.LoadType.TRACK_LOADED -> {
|
||||||
player.playTrack(item.track)
|
gts.queue(item.track)
|
||||||
if (!silent)
|
if (!silent)
|
||||||
MessageUtil.sendEmbedForInteraction(
|
MessageUtil.sendEmbedForInteractionWithImage(
|
||||||
interaction, response,
|
interaction,
|
||||||
"Playing track from link",
|
response,
|
||||||
|
"Queuing track from link",
|
||||||
"**${item.track.info.title}**\n*Queue*\nby ${item.track.info.author} :: ${
|
"**${item.track.info.title}**\n*Queue*\nby ${item.track.info.author} :: ${
|
||||||
TimeUtil.getTimeFormatedRaw(
|
TimeUtil.getTimeFormatedRaw(
|
||||||
item.track.info.length
|
item.track.info.length
|
||||||
)
|
)
|
||||||
}"
|
}\n" +
|
||||||
|
">>>${item.track.info.uri}",
|
||||||
|
"https://img.youtube.com/vi/" + UrlUtil.getYtThumbnailUrl(item.track.info.uri) + "/maxresdefault.jpg"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackResponse.LoadType.PLAYLIST_LOADED -> {
|
TrackResponse.LoadType.PLAYLIST_LOADED -> {
|
||||||
val l = item.tracks.asReversed()
|
val l = item.tracks.reversed()
|
||||||
for (p in l) {
|
for (partialTrack in l) {
|
||||||
player.playTrack(p)
|
gts.queue(partialTrack)
|
||||||
}
|
}
|
||||||
if (!silent)
|
if (!silent)
|
||||||
MessageUtil.sendEmbedForInteraction(
|
MessageUtil.sendEmbedForInteractionWithImage(
|
||||||
interaction, response,
|
interaction,
|
||||||
"Playing playlist from link",
|
response,
|
||||||
|
"Queuing playlist from link",
|
||||||
"**${item.tracks.first().info.title}**\n*${item.playlistInfo.name}*\nby ${item.tracks.first().info.author} :: ${
|
"**${item.tracks.first().info.title}**\n*${item.playlistInfo.name}*\nby ${item.tracks.first().info.author} :: ${
|
||||||
TimeUtil.getTimeFormatedRaw(
|
TimeUtil.getTimeFormatedRaw(
|
||||||
item.tracks.first().info.length
|
item.tracks.first().info.length
|
||||||
)
|
)
|
||||||
}"
|
}\n" +
|
||||||
|
">>>${item.tracks.first().info.uri}",
|
||||||
|
"https://img.youtube.com/vi/" + UrlUtil.getYtThumbnailUrl(item.tracks.first().info.uri) + "/maxresdefault.jpg"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackResponse.LoadType.SEARCH_RESULT -> {
|
TrackResponse.LoadType.SEARCH_RESULT -> {
|
||||||
player.playTrack(item.tracks.first())
|
gts.queue(item.tracks.first())
|
||||||
if (!silent)
|
if (!silent)
|
||||||
MessageUtil.sendEmbedForInteraction(
|
MessageUtil.sendEmbedForInteractionWithImage(
|
||||||
interaction, response,
|
interaction,
|
||||||
"Playing from query",
|
response,
|
||||||
|
"Queuing track from query",
|
||||||
"**${item.tracks.first().info.title}**\n*Queue*\nby ${item.tracks.first().info.author} :: ${
|
"**${item.tracks.first().info.title}**\n*Queue*\nby ${item.tracks.first().info.author} :: ${
|
||||||
TimeUtil.getTimeFormatedRaw(
|
TimeUtil.getTimeFormatedRaw(
|
||||||
item.tracks.first().info.length
|
item.tracks.first().info.length
|
||||||
)
|
)
|
||||||
}"
|
}\n" +
|
||||||
|
">>>${item.tracks.first().info.uri}",
|
||||||
|
"https://img.youtube.com/vi/" + UrlUtil.getYtThumbnailUrl(item.tracks.first().info.uri) + "/maxresdefault.jpg"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackResponse.LoadType.NO_MATCHES -> {
|
TrackResponse.LoadType.NO_MATCHES -> {
|
||||||
if (!silent)
|
if (!silent)
|
||||||
MessageUtil.sendEmbedForInteraction(interaction, response, "Not found", "There were not matches.")
|
MessageUtil.sendEmbedForInteraction(interaction, response, "Not found", "There were no matches.")
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackResponse.LoadType.LOAD_FAILED -> {
|
TrackResponse.LoadType.LOAD_FAILED -> {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Botendo
|
|
||||||
* Copyright (C) 2023 limited_dev
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package de.limited_dev.botendo.commands.slash.music.component
|
|
||||||
|
|
||||||
class TrackScheduler {
|
|
||||||
}
|
|
|
@ -22,11 +22,13 @@ package de.limited_dev.botendo.util
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object TokenManager {
|
object CredentialManager {
|
||||||
private const val filename = "token.nils"
|
private const val filename = "credentials.nils"
|
||||||
var token: String = "empty"
|
var token: String = "empty"
|
||||||
|
var linkip: String = "empty"
|
||||||
|
var linkpw: String = "empty"
|
||||||
|
|
||||||
///Load the bot token, generate a config if there is none
|
///Load the needed credentials, generate a config if there is none
|
||||||
fun load() {
|
fun load() {
|
||||||
val configFile = File(filename)
|
val configFile = File(filename)
|
||||||
if (!configFile.exists()) {
|
if (!configFile.exists()) {
|
||||||
|
@ -38,6 +40,8 @@ object TokenManager {
|
||||||
val prop = Properties()
|
val prop = Properties()
|
||||||
prop.load(input)
|
prop.load(input)
|
||||||
token = if (prop.getProperty("token").equals("empty")) "empty" else prop.getProperty("token")
|
token = if (prop.getProperty("token").equals("empty")) "empty" else prop.getProperty("token")
|
||||||
|
linkip = if (prop.getProperty("lavaip").equals("empty")) "empty" else prop.getProperty("lavaip")
|
||||||
|
linkpw = if (prop.getProperty("lavapw").equals("empty")) "empty" else prop.getProperty("lavapw")
|
||||||
input.close()
|
input.close()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
@ -58,6 +62,8 @@ object TokenManager {
|
||||||
val output: OutputStream = FileOutputStream(filename)
|
val output: OutputStream = FileOutputStream(filename)
|
||||||
val prop = Properties()
|
val prop = Properties()
|
||||||
prop.setProperty("token", "empty")
|
prop.setProperty("token", "empty")
|
||||||
|
prop.setProperty("lavaip", "empty")
|
||||||
|
prop.setProperty("lavapw", "empty")
|
||||||
prop.store(output, null)
|
prop.store(output, null)
|
||||||
output.close()
|
output.close()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
|
@ -65,7 +65,7 @@ object MessageUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEmbed(title: String, description: String, source: String): EmbedBuilder {
|
private fun getEmbed(title: String, description: String, source: String): EmbedBuilder {
|
||||||
val now: LocalDateTime = LocalDateTime.now()
|
val now: LocalDateTime = LocalDateTime.now()
|
||||||
val ebb = EmbedBuilder()
|
val ebb = EmbedBuilder()
|
||||||
ebb.title = title
|
ebb.title = title
|
||||||
|
@ -75,7 +75,12 @@ object MessageUtil {
|
||||||
return ebb
|
return ebb
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEmbedWithImage(title: String, description: String, source: String, thumbnailUrl: String): EmbedBuilder {
|
private fun getEmbedWithImage(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
source: String,
|
||||||
|
thumbnailUrl: String
|
||||||
|
): EmbedBuilder {
|
||||||
val ebb = getEmbed(title, description, source)
|
val ebb = getEmbed(title, description, source)
|
||||||
ebb.thumbnail = EmbedBuilder.Thumbnail()
|
ebb.thumbnail = EmbedBuilder.Thumbnail()
|
||||||
ebb.thumbnail!!.url = thumbnailUrl
|
ebb.thumbnail!!.url = thumbnailUrl
|
||||||
|
|
|
@ -22,8 +22,9 @@ package de.limited_dev.botendo.util
|
||||||
import de.limited_dev.botendo.Bot
|
import de.limited_dev.botendo.Bot
|
||||||
|
|
||||||
object Status {
|
object Status {
|
||||||
|
//TODO: impl.
|
||||||
suspend fun updateStatus() {
|
suspend fun updateStatus() {
|
||||||
Bot.kord!!.editPresence {
|
Bot.kord.editPresence {
|
||||||
this.playing("")
|
this.playing("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package de.limited_dev.botendo.commands.slash.music.component
|
package de.limited_dev.botendo.util
|
||||||
|
|
||||||
class GuildMusicManager {
|
object UrlUtil {
|
||||||
|
fun getYtThumbnailUrl(uri: String): String {
|
||||||
|
return uri.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue