Skip to content

Commit aa58021

Browse files
committed
feat: Add music feature using SoundCloud and Discord CDN as default sources
1 parent edf436b commit aa58021

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1174
-30
lines changed

common/src/commonMain/kotlin/net/cakeyfox/common/Constants.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ object Constants {
2727
const val TOKEN_ENDPOINT = "https://discord.com/api/oauth2/token"
2828
const val DEFAULT_ENDPOINT = "https://discord.com/api/users/@me"
2929

30+
// Last.fm
31+
const val LASTFM_API = "https://ws.audioscrobbler.com/2.0/"
32+
3033
@OptIn(ExperimentalSerializationApi::class)
3134
val HOCON = Hocon { useArrayPolymorphism = true }
3235
const val SUPPORT_SERVER_ID = "768267522670723094"
@@ -60,7 +63,6 @@ object Constants {
6063
}
6164

6265
/* ---- [Profile Assets] ---- */
63-
6466
fun getProfileBackground(backgroundId: String): String {
6567
return "https://stuff.foxybot.xyz/backgrounds/$backgroundId"
6668
}

common/src/commonMain/kotlin/net/cakeyfox/common/FoxyEmotes.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ object FoxyEmotes {
7272
const val SALLY = "<:sally:1392547806937481278>"
7373
const val RDRBR = "<:rdrbr:1392547804823425024>"
7474

75+
// Music platforms
76+
const val YOUTUBE = "<:youtube:1092607821771710584>"
77+
const val SOUNDCLOUD = "<:soundcloud:1416909085609037968>"
78+
const val DEEZER = "<:deezer:1416909400009871550>"
79+
const val DISCORD = "<:online:1304276804885938197>"
80+
7581
val badgesMap = mapOf(
7682
"artists" to ARTISTS,
7783
"banned" to BANNED,

foxy/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ dependencies {
1616
implementation(libs.jda)
1717
implementation("com.github.freya022:jda-ktx:8929de93af")
1818

19+
// Music
20+
implementation(libs.lavalink.client)
21+
1922
// Coroutines and DateTime
2023
implementation(libs.kotlinx.coroutines.core)
2124
implementation(libs.kotlinx.coroutines.debug)

foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package net.cakeyfox.foxy
22

3+
import dev.arbjerg.lavalink.client.LavalinkClient
4+
import dev.arbjerg.lavalink.client.getUserIdFromToken
5+
import dev.arbjerg.lavalink.client.loadbalancing.builtin.VoiceRegionPenaltyProvider
6+
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener
37
import io.ktor.client.*
48
import io.ktor.client.engine.cio.*
59
import io.ktor.client.plugins.*
@@ -23,11 +27,14 @@ import net.cakeyfox.foxy.listeners.MessageListener
2327
import net.cakeyfox.serializable.data.utils.FoxyConfig
2428
import net.cakeyfox.foxy.utils.FoxyUtils
2529
import net.cakeyfox.foxy.database.DatabaseClient
30+
import net.cakeyfox.foxy.utils.music.GuildMusicManager
2631
import net.cakeyfox.foxy.utils.threads.ThreadPoolManager
2732
import net.cakeyfox.foxy.utils.threads.ThreadUtils
2833
import net.cakeyfox.foxy.leaderboard.LeaderboardManager
2934
import net.cakeyfox.foxy.utils.TasksUtils
3035
import net.cakeyfox.foxy.internal.FoxyInternalAPI
36+
import net.cakeyfox.foxy.listeners.lavalink.LavalinkMajorListener
37+
import net.cakeyfox.foxy.utils.LavalinkUtils.registerNode
3138
import net.cakeyfox.foxy.utils.youtube.YouTubeManager
3239
import net.dv8tion.jda.api.JDAInfo
3340
import net.dv8tion.jda.api.OnlineStatus
@@ -58,6 +65,7 @@ class FoxyInstance(
5865
encodeDefaults = true
5966
ignoreUnknownKeys = true
6067
}
68+
6169
val environment = config.environment
6270
val restVersion = JDAInfo.DISCORD_REST_VERSION
6371
val baseUrl = config.discord.baseUrl
@@ -70,6 +78,9 @@ class FoxyInstance(
7078
val youtubeManager: YouTubeManager by lazy { YouTubeManager(this) }
7179
val database: DatabaseClient by lazy { DatabaseClient(this).also { it.start() } }
7280
val commandHandler: FoxyCommandManager by lazy { FoxyCommandManager(this) }
81+
val lavalink = LavalinkClient(getUserIdFromToken(config.discord.token))
82+
val musicManagers = mutableMapOf<Long, GuildMusicManager>()
83+
7384
val http: HttpClient by lazy {
7485
HttpClient(CIO) {
7586
install(HttpTimeout) { requestTimeoutMillis = 60_000 }
@@ -78,6 +89,7 @@ class FoxyInstance(
7889
}
7990

8091
suspend fun start() {
92+
lavalink.loadBalancer.addPenaltyProvider(VoiceRegionPenaltyProvider())
8193
utils = FoxyUtils(this)
8294
interactionManager = FoxyComponentManager(this)
8395
showtimeClient = ShowtimeClient(config, config.showtime.key)
@@ -89,7 +101,8 @@ class FoxyInstance(
89101
GatewayIntent.GUILD_MESSAGES,
90102
GatewayIntent.SCHEDULED_EVENTS,
91103
GatewayIntent.GUILD_EXPRESSIONS,
92-
GatewayIntent.DIRECT_MESSAGES
104+
GatewayIntent.DIRECT_MESSAGES,
105+
GatewayIntent.GUILD_VOICE_STATES
93106
).apply {
94107
if (baseUrl != null) {
95108
logger.info { "Using Discord base URL: $baseUrl" }
@@ -100,8 +113,10 @@ class FoxyInstance(
100113
.addEventListeners(
101114
GuildListener(this),
102115
InteractionsListener(this),
103-
MessageListener(this)
116+
MessageListener(this),
117+
LavalinkMajorListener(lavalink, this)
104118
)
119+
.setVoiceDispatchInterceptor(JDAVoiceUpdateListener((lavalink)))
105120
.setAutoReconnect(true)
106121
.setStatus(OnlineStatus.fromKey(database.bot.getBotSettings().status))
107122
.setActivity(
@@ -122,12 +137,14 @@ class FoxyInstance(
122137
CacheFlag.EMOJI,
123138
CacheFlag.STICKER,
124139
CacheFlag.MEMBER_OVERRIDES,
140+
CacheFlag.VOICE_STATE
125141
)
126142
.setToken(config.discord.token)
127143
.setEnableShutdownHook(false)
128144
.build()
129145

130146
leaderboardManager.startAutoRefresh()
147+
registerNode(this)
131148
if (currentCluster.isMasterCluster) TasksUtils.launchTasks(this)
132149
this.commandHandler.handle()
133150

foxy/src/main/kotlin/net/cakeyfox/foxy/database/utils/UserUtils.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package net.cakeyfox.foxy.database.utils
22

33
import com.mongodb.client.model.Filters.and
4-
import kotlinx.datetime.toJavaInstant
54
import net.cakeyfox.foxy.database.DatabaseClient
65
import org.bson.Document
76
import kotlinx.coroutines.flow.firstOrNull
@@ -14,6 +13,7 @@ import com.mongodb.client.model.Projections.include
1413
import com.mongodb.client.model.Sorts.ascending
1514
import kotlinx.coroutines.flow.map
1615
import kotlinx.coroutines.flow.toList
16+
import kotlinx.datetime.toJavaInstant
1717
import net.cakeyfox.foxy.FoxyInstance
1818
import net.cakeyfox.foxy.database.data.*
1919
import net.cakeyfox.serializable.data.utils.UserBalance
@@ -22,6 +22,8 @@ import java.time.ZoneId
2222
import java.time.ZonedDateTime
2323
import java.time.temporal.ChronoUnit
2424
import java.util.Date
25+
import kotlin.time.ExperimentalTime
26+
import kotlin.time.toJavaInstant
2527

2628
class UserUtils(
2729
private val client: DatabaseClient,

foxy/src/main/kotlin/net/cakeyfox/foxy/interactions/FoxyCommandManager.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import net.cakeyfox.foxy.interactions.vanilla.entertainment.declarations.RateWai
2121
import net.cakeyfox.foxy.interactions.vanilla.entertainment.declarations.RollsCommand
2222
import net.cakeyfox.foxy.interactions.vanilla.entertainment.declarations.RussianRouletteCommand
2323
import net.cakeyfox.foxy.interactions.vanilla.magic.declarations.MagicCommand
24+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.NowPlayingCommand
25+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.PauseCommand
26+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.PlayCommand
27+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.QueueCommand
28+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.SkipCommand
29+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.StopCommand
30+
import net.cakeyfox.foxy.interactions.vanilla.music.declarations.VolumeCommand
2431
import net.cakeyfox.foxy.interactions.vanilla.social.declarations.BirthdayCommand
2532
import net.cakeyfox.foxy.interactions.vanilla.social.declarations.DivorceCommand
2633
import net.cakeyfox.foxy.interactions.vanilla.social.declarations.MarryCommand
@@ -122,9 +129,6 @@ class FoxyCommandManager(private val foxy: FoxyInstance) {
122129

123130

124131
init {
125-
/* ---- [Roleplay] ---- */
126-
register(RoleplayCommand())
127-
128132
/* ---- [Economy] ---- */
129133
register(CakesCommand())
130134
register(DailyCommand())
@@ -146,6 +150,16 @@ class FoxyCommandManager(private val foxy: FoxyInstance) {
146150
register(MarryCommand())
147151
register(DivorceCommand())
148152
register(BirthdayCommand())
153+
register(RoleplayCommand())
154+
155+
/* ---- [Music] ---- */
156+
register(PlayCommand())
157+
register(SkipCommand())
158+
register(StopCommand())
159+
register(NowPlayingCommand())
160+
register(PauseCommand())
161+
register(VolumeCommand())
162+
register(QueueCommand())
149163

150164
/* ---- [Discord] ---- */
151165
register(UserCommand())
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package net.cakeyfox.foxy.interactions
22

3-
fun pretty(emoji: String, content: String): String = "$emoji **|** $content"
3+
fun pretty(emoji: String, content: String, separator: String? = "**|**"): String = "$emoji $separator $content"

foxy/src/main/kotlin/net/cakeyfox/foxy/interactions/commands/CommandCategory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ object CommandCategory {
88
const val SOCIAL = "social"
99
const val UTILS = "utils"
1010
const val MAGIC = "magic"
11+
const val MUSIC = "music"
1112
const val DISCORD = "discord"
1213
const val YOUTUBE = "youtube"
1314
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package net.cakeyfox.foxy.interactions.vanilla.music
2+
3+
import net.cakeyfox.common.Colors
4+
import net.cakeyfox.common.FoxyEmotes
5+
import net.cakeyfox.foxy.interactions.commands.CommandContext
6+
import net.cakeyfox.foxy.interactions.commands.UnleashedCommandExecutor
7+
import net.cakeyfox.foxy.interactions.pretty
8+
9+
class NowPlayingExecutor : UnleashedCommandExecutor() {
10+
override suspend fun execute(context: CommandContext) {
11+
val link = context.foxy.lavalink.getOrCreateLink(context.guild!!.idLong)
12+
val player = link.cachedPlayer
13+
14+
if (player == null) {
15+
context.reply(true) {
16+
content = pretty(FoxyEmotes.FoxyCry, context.locale["music.nowPlaying.nothingPlaying"])
17+
}
18+
return
19+
}
20+
21+
val track = player.track
22+
23+
if (track == null) {
24+
context.reply(true) {
25+
content = pretty(FoxyEmotes.FoxyCry, context.locale["music.nowPlaying.nothingPlaying"])
26+
}
27+
return
28+
}
29+
30+
val trackInfo = track.info
31+
32+
val title = trackInfo.title
33+
val author = trackInfo.author
34+
val uri = trackInfo.uri ?: context.locale["music.nowPlaying.unknownUri"]
35+
val duration = if (trackInfo.length >= 0) {
36+
val totalSeconds = trackInfo.length / 1000
37+
val minutes = totalSeconds / 60
38+
val seconds = totalSeconds % 60
39+
String.format("%d:%02d", minutes, seconds)
40+
} else context.locale["music.nowPlaying.unknownDuration"]
41+
42+
43+
context.reply {
44+
embed {
45+
color = Colors.BLUE
46+
this.title = pretty("🎵", context.locale["music.nowPlaying.nowPlaying"])
47+
this.description = "**[$title]($uri)**"
48+
49+
this.field {
50+
name = context.locale["music.nowPlaying.byAuthor"]
51+
value = "**$author**"
52+
inline = false
53+
}
54+
55+
this.field {
56+
name = context.locale["music.nowPlaying.duration"]
57+
value = "`$duration`"
58+
inline = true
59+
}
60+
61+
this.field {
62+
name = context.locale["music.nowPlaying.volume"]
63+
value = "`${player.volume}%`"
64+
inline = true
65+
}
66+
67+
this.thumbnail = trackInfo.artworkUrl
68+
}
69+
}
70+
}
71+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package net.cakeyfox.foxy.interactions.vanilla.music
2+
3+
import net.cakeyfox.common.FoxyEmotes
4+
import net.cakeyfox.foxy.interactions.commands.CommandContext
5+
import net.cakeyfox.foxy.interactions.commands.UnleashedCommandExecutor
6+
import net.cakeyfox.foxy.interactions.pretty
7+
import net.cakeyfox.foxy.utils.music.getOrCreateMusicManager
8+
9+
class PauseExecutor : UnleashedCommandExecutor() {
10+
override suspend fun execute(context: CommandContext) {
11+
val channel = context.member!!.voiceState?.channel
12+
val lavalink = context.foxy.lavalink
13+
14+
if (channel == null) {
15+
context.reply(true) {
16+
content = pretty(FoxyEmotes.FoxyRage, context.locale["music.play.userNotInVoiceChannel"])
17+
}
18+
return
19+
}
20+
21+
val manager = getOrCreateMusicManager(context.guild!!.idLong, lavalink, context, channel)
22+
val player = manager.getPlayer()
23+
24+
if (player?.track == null) {
25+
context.reply(true) {
26+
content = pretty(FoxyEmotes.FoxyCry, context.locale["music.pause.nothingPlaying"])
27+
}
28+
return
29+
}
30+
31+
val isPaused = manager.scheduler.pauseOrResume() ?: return
32+
33+
if (isPaused) {
34+
context.reply(true) {
35+
content = pretty(FoxyEmotes.FoxyYay, context.locale["music.pause.paused"])
36+
}
37+
} else {
38+
context.reply(true) {
39+
content = pretty(FoxyEmotes.FoxyOk, context.locale["music.pause.resumed"])
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)