Skip to content

Commit be7df32

Browse files
committed
Use java-lyrics to give timed lyrics
1 parent d1e47ac commit be7df32

File tree

4 files changed

+147
-24
lines changed

4 files changed

+147
-24
lines changed

bot/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies {
5555
implementation(libs.logback.newSlf4j)
5656
implementation(libs.spotify)
5757
implementation(libs.youtube)
58+
implementation(libs.javaLyrics)
5859
implementation(libs.bundles.featureLibs)
5960

6061
implementation(kotlin("stdlib"))

bot/src/main/kotlin/me/duncte123/skybot/commands/music/LyricsCommand.kt

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,25 @@
1919
package me.duncte123.skybot.commands.music
2020

2121
import com.github.natanbc.reliqua.limiter.RateLimiter
22-
import me.duncte123.botcommons.StringUtils
22+
import dev.arbjerg.lavalink.client.Link
2323
import me.duncte123.botcommons.messaging.EmbedUtils
2424
import me.duncte123.botcommons.messaging.MessageUtils.sendEmbed
2525
import me.duncte123.botcommons.messaging.MessageUtils.sendMsg
2626
import me.duncte123.botcommons.web.WebParserUtils
2727
import me.duncte123.botcommons.web.WebUtils
28+
import me.duncte123.lyrics.model.Lyrics
29+
import me.duncte123.lyrics.model.TextLyrics
30+
import me.duncte123.lyrics.model.TimedLyrics
2831
import me.duncte123.skybot.Variables
2932
import me.duncte123.skybot.objects.command.CommandContext
3033
import me.duncte123.skybot.objects.command.MusicCommand
3134
import me.duncte123.skybot.objects.config.DunctebotConfig
35+
import me.duncte123.skybot.utils.chunkForEmbed
3236
import net.dv8tion.jda.api.EmbedBuilder
3337
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
3438
import net.dv8tion.jda.api.interactions.commands.OptionType
3539
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData
40+
import reactor.core.scheduler.Schedulers
3641
import java.net.URLEncoder
3742
import java.nio.charset.StandardCharsets
3843

@@ -49,6 +54,7 @@ class LyricsCommand : MusicCommand() {
4954
val args = ctx.args
5055

5156
if (args.isNotEmpty()) {
57+
// TODO: search with lavalink for lyrics
5258
handleSearch(ctx.argsRaw, ctx.config) {
5359
if (it == null) {
5460
sendMsg(ctx, "There where no lyrics found for `${ctx.argsRaw}`")
@@ -68,13 +74,20 @@ class LyricsCommand : MusicCommand() {
6874
return
6975
}
7076

71-
val search = playingTrack.info.title.trim()
72-
73-
// just search for the title, the author might be a weird youtube channel
74-
handleSearch(search, ctx.config) {
77+
loadLyricsFromLavalink(player.link) {
7578
if (it == null) {
76-
sendMsg(ctx, "There where no lyrics found for `$search`")
77-
return@handleSearch
79+
// TODO: fallback for genius
80+
val searchItem = "${playingTrack.info.title} - ${playingTrack.info.author}"
81+
82+
handleSearch(searchItem, ctx.config) { embed ->
83+
if (embed == null) {
84+
sendMsg(ctx, "There where no lyrics found for `${playingTrack.info.title}`")
85+
return@handleSearch
86+
}
87+
88+
sendEmbed(ctx, embed)
89+
}
90+
return@loadLyricsFromLavalink
7891
}
7992

8093
sendEmbed(ctx, it)
@@ -105,25 +118,32 @@ class LyricsCommand : MusicCommand() {
105118

106119
event.deferReply().queue()
107120

108-
val search = playingTrack.info.title.trim()
109-
110-
// just search for the title, the author might be a weird youtube channel
111-
handleSearch(search, variables.config) {
121+
loadLyricsFromLavalink(player.link) {
112122
if (it == null) {
113-
event.hook.sendMessage("There where no lyrics found for `$search`").queue()
114-
return@handleSearch
123+
val searchItem = "${playingTrack.info.title} - ${playingTrack.info.author}"
124+
125+
handleSearch(searchItem, variables.config) { embed ->
126+
if (embed == null) {
127+
event.hook.sendMessage("There where no lyrics found for `${playingTrack.info.title}`")
128+
.queue()
129+
return@handleSearch
130+
}
131+
132+
event.hook.sendMessageEmbeds(embed.build()).queue()
133+
}
134+
return@loadLyricsFromLavalink
115135
}
116136

117137
event.hook.sendMessageEmbeds(it.build()).queue()
118138
}
119-
120139
return
121140
}
122141

123142
event.deferReply().queue()
124143

125144
val search = opt.asString
126145

146+
// TODO: search with lavalink for lyrics
127147
handleSearch(search, variables.config) {
128148
if (it == null) {
129149
event.hook.sendMessage("There where no lyrics found for `$search`").queue()
@@ -134,21 +154,76 @@ class LyricsCommand : MusicCommand() {
134154
}
135155
}
136156

157+
private fun loadLyricsFromLavalink(link: Link, cb: (EmbedBuilder?) -> Unit) {
158+
val sessionId = link.node.sessionId!!
159+
val guildId = link.guildId
160+
161+
link.node.customJsonRequest(Lyrics::class.java) {
162+
it.path("/v4/sessions/$sessionId/players/$guildId/lyrics")
163+
}
164+
.publishOn(Schedulers.boundedElastic())
165+
.doOnError { cb(null) }
166+
.doOnSuccess {
167+
val lyricInfo = when (it) {
168+
is TimedLyrics -> {
169+
// Block is safe here, player is already cached
170+
val position = link.getPlayer().block()!!.state.position
171+
172+
val text = buildString {
173+
it.lines.forEach { line ->
174+
if (line.range.start <= position && position <= line.range.end) {
175+
append("__**${line.line}**__\n")
176+
} else {
177+
append("${line.line}\n")
178+
}
179+
}
180+
}
181+
182+
LyricInfo(
183+
it.track.albumArt.last().url,
184+
it.track.title,
185+
null,
186+
text
187+
)
188+
}
189+
190+
is TextLyrics -> LyricInfo(
191+
it.track.albumArt.last().url,
192+
it.track.title,
193+
null,
194+
it.text
195+
)
196+
197+
else -> null
198+
}
199+
200+
lyricInfo?.let { info ->
201+
cb(buildLyricsEmbed(info))
202+
}
203+
}
204+
.subscribe()
205+
}
206+
207+
private fun buildLyricsEmbed(data: LyricInfo): EmbedBuilder {
208+
val builder = EmbedUtils.getDefaultEmbed()
209+
.setTitle("Lyrics for ${data.title}", data.url)
210+
.setThumbnail(data.artUrl)
211+
212+
data.lyrics.chunkForEmbed(450).forEachIndexed { index, chunk ->
213+
builder.addField("**[${index + 1}]**", chunk, true)
214+
}
215+
216+
return builder
217+
}
218+
137219
private fun handleSearch(search: String, config: DunctebotConfig, cb: (EmbedBuilder?) -> Unit) {
138220
searchForSong(search, config) {
139221
if (it == null) {
140222
cb(null)
141223
return@searchForSong
142224
}
143225

144-
cb(
145-
EmbedUtils.getDefaultEmbed()
146-
.setTitle("Lyrics for $search", it.url)
147-
.setThumbnail(it.art)
148-
.setDescription(StringUtils.abbreviate(it.lyrics, 1900))
149-
.appendDescription("\n\n Full lyrics on [genuis.com](${it.url})")
150-
.setFooter("Powered by genuis.com")
151-
)
226+
cb(buildLyricsEmbed(it))
152227
}
153228
}
154229

@@ -189,6 +264,7 @@ class LyricsCommand : MusicCommand() {
189264
callback(
190265
LyricInfo(
191266
data["song_art_image_url"].asText(),
267+
"",
192268
data["url"].asText(),
193269
lyrics
194270
)
@@ -205,7 +281,7 @@ class LyricsCommand : MusicCommand() {
205281
val text = lyricsContainer.first()!!
206282
.wholeText()
207283
.replace("<br>", "\n")
208-
.replace("\n\n\n", "\n\n")
284+
.replace("\n\n\n", "\n")
209285
.trim()
210286

211287
callback(text)
@@ -214,5 +290,5 @@ class LyricsCommand : MusicCommand() {
214290
}
215291
}
216292

217-
private data class LyricInfo(val art: String, val url: String, val lyrics: String)
293+
private data class LyricInfo(val artUrl: String, val title: String, val url: String?, val lyrics: String)
218294
}

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ fun VersionCatalogBuilder.bot() {
7676
library("botCommons", "me.duncte123", "botCommons").version("3.0.16")
7777

7878
bundle("featureLibs", listOf("botCommons", "weebjava", "loadingBar", "jagTag", "wolfram-alpha", "duration-parser", "emoji-java"))
79+
80+
library("javaLyrics", "com.github.DuncteBot.java-timed-lyrics", "protocol").version("1.2.0")
7981
}
8082

8183
fun VersionCatalogBuilder.database() {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Skybot, a multipurpose discord bot
3+
* Copyright (C) 2017 Duncan "duncte123" Sterken & Ramid "ramidzkh" Khan & Maurice R S "Sanduhr32"
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
@file:JvmName("TextUtils")
20+
21+
package me.duncte123.skybot.utils
22+
23+
/**
24+
* Slices text into chunks to make it manageable
25+
*/
26+
fun String.chunkForEmbed(limit: Int = 2000): List<String> {
27+
val lines = this.split("\n")
28+
val chunks = mutableListOf<String>()
29+
30+
var chunk = ""
31+
lines.forEach { line ->
32+
if (chunk.length + line.length > limit && chunk.isNotEmpty()) {
33+
chunks.add(chunk)
34+
chunk = ""
35+
}
36+
if (line.length > limit) {
37+
line.chunked(limit).forEach { chunks.add(it) }
38+
} else {
39+
chunk += "$line\n"
40+
}
41+
}
42+
43+
return chunks
44+
}

0 commit comments

Comments
 (0)