From 9ebf59de7650b4af0b53a8b63cdfb5ce55c211be Mon Sep 17 00:00:00 2001 From: Michael Rittmeister <michael@rittmeister.in> Date: Fri, 22 Dec 2023 00:06:54 +0100 Subject: [PATCH] Add lyrics plugin --- .../mikbot/plugin/api/util/Confirmation.kt | 12 +- gradle/libs.versions.toml | 6 +- music/build.gradle.kts | 2 +- music/commands/build.gradle.kts | 1 - .../schlaubi/mikmusic/commands/Commands.kt | 5 - .../mikmusic/commands/LycricsCommand.kt | 60 ------- music/lyrics/build.gradle.kts | 37 ++++ music/lyrics/src/main/kotlin/APIServer.kt | 165 ++++++++++++++++++ music/lyrics/src/main/kotlin/Config.kt | 7 + .../lyrics/src/main/kotlin/KaraokeCommand.kt | 34 ++++ music/lyrics/src/main/kotlin/LyricsCommand.kt | 74 ++++++++ music/lyrics/src/main/kotlin/LyricsPlugin.kt | 24 +++ music/lyrics/src/main/kotlin/events/Events.kt | 46 +++++ .../lyrics/strings_de_DE.properties | 4 + .../lyrics/strings_en_GB.properties | 4 + music/player/build.gradle.kts | 1 + .../autocomplete/AutoCompleteArgument.kt | 6 +- .../mikmusic/player/queue/QueueResponse.kt | 2 +- runtime/plugins.txt | 4 +- settings.gradle.kts | 1 + 20 files changed, 418 insertions(+), 77 deletions(-) delete mode 100644 music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/LycricsCommand.kt create mode 100644 music/lyrics/build.gradle.kts create mode 100644 music/lyrics/src/main/kotlin/APIServer.kt create mode 100644 music/lyrics/src/main/kotlin/Config.kt create mode 100644 music/lyrics/src/main/kotlin/KaraokeCommand.kt create mode 100644 music/lyrics/src/main/kotlin/LyricsCommand.kt create mode 100644 music/lyrics/src/main/kotlin/LyricsPlugin.kt create mode 100644 music/lyrics/src/main/kotlin/events/Events.kt create mode 100644 music/lyrics/src/main/resources/translations/lyrics/strings_de_DE.properties create mode 100644 music/lyrics/src/main/resources/translations/lyrics/strings_en_GB.properties diff --git a/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/Confirmation.kt b/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/Confirmation.kt index d3db6c8f3..4e1d286a0 100644 --- a/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/Confirmation.kt +++ b/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/Confirmation.kt @@ -16,7 +16,7 @@ import dev.kord.core.entity.interaction.ComponentInteraction import dev.kord.core.entity.interaction.followup.EphemeralFollowupMessage import dev.kord.core.entity.interaction.followup.PublicFollowupMessage import dev.kord.core.event.interaction.InteractionCreateEvent -import dev.kord.rest.builder.message.create.actionRow +import dev.kord.rest.builder.message.actionRow import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -117,7 +117,15 @@ private suspend fun CommandContext.confirmation( timeout: Duration = 30.seconds, acknowledge: Boolean = true, messageBuilder: MessageBuilder, -): Confirmation = confirmation(sendMessage, timeout, messageBuilder, translate = ::translate, yesWord = yesWord, noWord = noWord, acknowledge = acknowledge) +): Confirmation = confirmation( + sendMessage, + timeout, + messageBuilder, + translate = ::translate, + yesWord = yesWord, + noWord = noWord, + acknowledge = acknowledge +) /** * Bare bone confirmation implementation. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 431e5a0da..ae3fe2029 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,12 +4,12 @@ kordex = "1.6.0-SNAPSHOT" kmongo = "4.11.0" coroutines = "1.7.3" serialization = "1.6.0" -ktor = "2.3.6" +ktor = "2.3.7" kord = "0.12.0" jjwt = "0.11.5" api = "3.26.0" ksp = "2.0.0-Beta1-1.0.14" -lavakord = "main-SNAPSHOT" +lavakord = "6.1.2" [libraries] kord-common = { group = "dev.kord", name = "kord-common", version.ref = "kord" } @@ -34,6 +34,7 @@ lavakord-kord = { group = "dev.schlaubi.lavakord", name = "kord", version.ref = lavakord-sponsorblock = { group = "dev.schlaubi.lavakord", name = "sponsorblock", version.ref = "lavakord" } lavakord-lavsrc = { group = "dev.schlaubi.lavakord", name = "lavasrc-jvm", version.ref = "lavakord" } lavakord-lavasearch = { group = "dev.schlaubi.lavakord", name = "lavasearch-jvm", version.ref = "lavakord" } +lavakord-lyrics = { group = "dev.schlaubi.lavakord", name = "lyrics-jvm", version.ref = "lavakord" } spotify = { group = "se.michaelthelin.spotify", name = "spotify-web-api-java", version = "8.0.0" } krontab = { group = "dev.inmo", name = "krontab", version = "0.10.0" } ksp-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" } @@ -53,6 +54,7 @@ ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = ktor-server-sessions = { group = "io.ktor", name = "ktor-server-sessions", version.ref = "ktor" } ktor-server-core = { group = "io.ktor", name = "ktor-server-core-jvm", version.ref = "ktor" } ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" } +ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } ktor-server-html-builder = { group = "io.ktor", name = "ktor-server-html-builder", version.ref = "ktor" } github-repositories = { group = "dev.nycode.github", name = "repositories", version = "1.0.0-SNAPSHOT" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.4.1" } diff --git a/music/build.gradle.kts b/music/build.gradle.kts index 2ed503892..82158f317 100644 --- a/music/build.gradle.kts +++ b/music/build.gradle.kts @@ -1,3 +1,3 @@ subprojects { - version = "3.4.1-SNAPSHOT" + version = "3.5.0-SNAPSHOT" } diff --git a/music/commands/build.gradle.kts b/music/commands/build.gradle.kts index 6adef367c..c00f509c9 100644 --- a/music/commands/build.gradle.kts +++ b/music/commands/build.gradle.kts @@ -18,7 +18,6 @@ tasks { freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers" } } - } mikbotPlugin { diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/Commands.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/Commands.kt index adb8c472a..ec00f5891 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/Commands.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/Commands.kt @@ -1,6 +1,5 @@ package dev.schlaubi.mikmusic.commands -import dev.schlaubi.mikmusic.core.Config import dev.schlaubi.mikmusic.core.MusicModule suspend fun MusicModule.commands() { @@ -19,8 +18,4 @@ suspend fun MusicModule.commands() { clearCommand() fixCommand() nextCommand() - - if (Config.HAPPI_KEY != null) { - lyricsCommand() - } } diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/LycricsCommand.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/LycricsCommand.kt deleted file mode 100644 index d621d9b13..000000000 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/LycricsCommand.kt +++ /dev/null @@ -1,60 +0,0 @@ -package dev.schlaubi.mikmusic.commands - -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import dev.schlaubi.mikbot.plugin.api.util.forList -import dev.schlaubi.mikmusic.checks.musicQuizAntiCheat -import dev.schlaubi.mikmusic.core.MusicModule -import dev.schlaubi.mikmusic.util.fetchLyrics -import dev.schlaubi.mikmusic.util.searchHappiSong -import dev.schlaubi.stdx.core.paginate - -class LyricsArguments : Arguments() { - val name by optionalString { - name = "song_name" - description = "commands.lyrics.arguments.song_name.description" - } -} - -suspend fun MusicModule.lyricsCommand() = publicSlashCommand(::LyricsArguments) { - name = "lyrics" - description = "commands.lyrics.description" - - check { - musicQuizAntiCheat(this@lyricsCommand) - } - - action { - val query = arguments.name ?: player.playingTrack?.info?.title - ?.replace("\\(.+(?!\\))".toRegex(), "") // replace anything in brackets like (official music video) - - if (query == null) { - respond { - content = translate("command.lyrics.no_song_playing") - } - - return@action - } - val trackResponse = searchHappiSong(query) - if (trackResponse.length != 1) { - respond { - content = translate("command.lyrics.no_lyrics") - } - return@action - } - val track = trackResponse.result!!.first() - val lyrics = track.fetchLyrics().result!! - - val lines = lyrics.lyrics.lines() - val paged = lines.paginate(2000) - - editingPaginator { - forList(user, paged, { it }, { _, _ -> track.track }) { - footer { - text = "© ${lyrics.copyrightLabel ?: ""} | ${lyrics.copyrightNotice}" - } - } - }.send() - } -} diff --git a/music/lyrics/build.gradle.kts b/music/lyrics/build.gradle.kts new file mode 100644 index 000000000..fd00e1689 --- /dev/null +++ b/music/lyrics/build.gradle.kts @@ -0,0 +1,37 @@ +import dev.schlaubi.mikbot.gradle.GenerateDefaultTranslationBundleTask +import java.util.* + +plugins { + `mikbot-module` + com.google.devtools.ksp + dev.schlaubi.mikbot.`gradle-plugin` + alias(libs.plugins.kotlinx.serialization) +} + +dependencies { + plugin(projects.music.player) + plugin(projects.core.ktor) + ktorDependency(libs.ktor.server.websockets) + ktorDependency(libs.ktor.server.cors) +} + +mikbotPlugin { + pluginId = "music-lyrics" + description = "Plugin providing lyrics for the music plugin" +} + +fun DependencyHandlerScope.ktorDependency(dependency: ProviderConvertible<*>) = ktorDependency(dependency.asProvider()) +fun DependencyHandlerScope.ktorDependency(dependency: Provider<*>) = implementation(dependency) { + exclude(module = "ktor-server-core") +} + + +tasks { + val generateDefaultResourceBundle by registering(GenerateDefaultTranslationBundleTask::class) { + defaultLocale = Locale("en", "GB") + } + + classes { + dependsOn(generateDefaultResourceBundle) + } +} diff --git a/music/lyrics/src/main/kotlin/APIServer.kt b/music/lyrics/src/main/kotlin/APIServer.kt new file mode 100644 index 000000000..7312dd15f --- /dev/null +++ b/music/lyrics/src/main/kotlin/APIServer.kt @@ -0,0 +1,165 @@ +package dev.schlaubi.mikmusic.lyrics + +import com.kotlindiscord.kord.extensions.ExtensibleBot +import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import dev.kord.cache.api.query +import dev.kord.common.annotation.KordExperimental +import dev.kord.common.annotation.KordUnsafe +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.VoiceStateData +import dev.kord.core.event.user.VoiceStateUpdateEvent +import dev.kord.core.on +import dev.schlaubi.lavakord.audio.TrackEndEvent +import dev.schlaubi.lavakord.audio.TrackStartEvent +import dev.schlaubi.lavakord.audio.on +import dev.schlaubi.lavakord.audio.player.Player +import dev.schlaubi.lavakord.plugins.lyrics.rest.requestLyrics +import dev.schlaubi.lyrics.protocol.TimedLyrics +import dev.schlaubi.mikbot.plugin.api.config.Environment +import dev.schlaubi.mikbot.util_plugins.ktor.api.KtorExtensionPoint +import dev.schlaubi.mikmusic.core.MusicModule +import dev.schlaubi.mikmusic.lyrics.events.Event +import dev.schlaubi.mikmusic.lyrics.events.NextTrackEvent +import dev.schlaubi.mikmusic.lyrics.events.PlayerStateUpdateEvent +import dev.schlaubi.mikmusic.lyrics.events.PlayerStoppedEvent +import dev.schlaubi.mikmusic.player.MusicPlayer +import io.ktor.http.* +import io.ktor.serialization.kotlinx.* +import io.ktor.server.application.* +import io.ktor.server.plugins.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.websocket.* +import io.ktor.util.* +import io.ktor.websocket.* +import kotlinx.coroutines.* +import kotlinx.datetime.Clock +import kotlinx.serialization.json.Json +import org.koin.core.component.inject +import org.pf4j.Extension +import kotlin.collections.set +import kotlin.time.Duration.Companion.seconds +import dev.schlaubi.mikbot.plugin.api.config.Config as BotConfig + +private val PLAYER = AttributeKey<MusicPlayer>("MUSIC_PLAYER") +private val authKeys = mutableMapOf<String, Snowflake>() + +fun requestToken(userId: Snowflake): String { + val key = generateNonce() + authKeys[key] = userId + return key +} + +@Extension +class APIServer : KtorExtensionPoint, KordExKoinComponent { + + private val bot by inject<ExtensibleBot>() + private val musicModule by lazy { bot.findExtension<MusicModule>()!! } + + private val ApplicationCall.userId: Snowflake + get() { + val header = request.authorization() ?: parameters["api_key"] ?: unauthorized() + return authKeys[header] ?: unauthorized() + } + + @OptIn(KordUnsafe::class, KordExperimental::class) + private suspend fun Snowflake.findPlayer(): MusicPlayer { + val voiceState = bot.kordRef.findVoiceState(this) ?: notFound() + val player = musicModule.getMusicPlayer(bot.kordRef.unsafe.guild(voiceState.guildId)) + + return player.takeIf { it.playingTrack != null } ?: notFound() + } + + override fun Application.apply() { + if (pluginOrNull(WebSockets) == null) { + install(WebSockets) { + contentConverter = KotlinxWebsocketSerializationConverter(Json) + } + } + if (pluginOrNull(CORS) == null && BotConfig.ENVIRONMENT == Environment.DEVELOPMENT) { + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + anyHost() + } + } + + routing { + route("lyrics") { + get("current") { + val player = call.userId.findPlayer() + + call.respond(player.player.requestLyrics().takeIf { it is TimedLyrics } ?: notFound()) + } + + route("events") { + intercept(ApplicationCallPipeline.Plugins) { + call.attributes.put(PLAYER, call.userId.findPlayer()) + proceed() + } + + webSocket { + val player = call.attributes[PLAYER].player + val listenerScope = this + launch { + var state = player.toState() + while (isActive) { + val newState = player.toState() + if (newState != state && state.playing) { + state = newState + sendSerialized<Event>(newState) + } + + delay(1.seconds) + } + } + + player.on<TrackEndEvent>(listenerScope) { + if (!reason.mayStartNext) { + sendSerialized<Event>(PlayerStoppedEvent) + } + } + player.on<TrackStartEvent>(listenerScope) { + sendSerialized<Event>(NextTrackEvent(player.position)) + } + bot.kordRef.on<VoiceStateUpdateEvent>(listenerScope) { + if (state.userId == call.userId && state.channelId == null && old?.channelId != null) { + close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Left voice channel")) + } + } + + // Wait for connection to die or websocket getting closed otherwise + awaitCancellation() + } + } + } + } + } + + override fun StatusPagesConfig.apply() { + exception<UnauthorizedException> { call, _ -> + call.respond(HttpStatusCode.Unauthorized) + } + } +} + +suspend fun Kord.findVoiceState(userId: Snowflake): VoiceStateData? { + return cache.query<VoiceStateData> { + VoiceStateData::channelId ne null + VoiceStateData::userId eq userId + }.singleOrNull() +} + +private class UnauthorizedException : RuntimeException() + +private fun unauthorized(): Nothing = throw UnauthorizedException() +private fun notFound(): Nothing = throw NotFoundException() + +private fun Player.toState() = PlayerStateUpdateEvent(!paused, position, Clock.System.now()) diff --git a/music/lyrics/src/main/kotlin/Config.kt b/music/lyrics/src/main/kotlin/Config.kt new file mode 100644 index 000000000..0f3a7189f --- /dev/null +++ b/music/lyrics/src/main/kotlin/Config.kt @@ -0,0 +1,7 @@ +package dev.schlaubi.mikmusic.lyrics + +import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig + +object Config : EnvironmentConfig() { + val LYRICS_WEB_URL by getEnv("http://localhost:3000") +} diff --git a/music/lyrics/src/main/kotlin/KaraokeCommand.kt b/music/lyrics/src/main/kotlin/KaraokeCommand.kt new file mode 100644 index 000000000..e3725c3bc --- /dev/null +++ b/music/lyrics/src/main/kotlin/KaraokeCommand.kt @@ -0,0 +1,34 @@ +package dev.schlaubi.mikmusic.lyrics + +import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand +import dev.schlaubi.lavakord.plugins.lyrics.rest.requestLyrics +import dev.schlaubi.lyrics.protocol.TimedLyrics +import dev.schlaubi.mikbot.plugin.api.util.discordError +import dev.schlaubi.mikmusic.checks.musicQuizAntiCheat +import dev.schlaubi.mikmusic.util.musicModule + +suspend fun Extension.karaokeCommand() = ephemeralSlashCommand { + name = "karaoke" + description = "commands.karaoke.description" + + check { + musicQuizAntiCheat(musicModule) + } + + action { + val player = with(musicModule) { player } + + val lyrics = runCatching { player.requestLyrics() }.getOrNull() + + if (lyrics !is TimedLyrics) { + discordError(translate("commands.karaoke.not_available")) + } + + val token = requestToken(user.id) + + respond { + content = translate("commands.karaoke.success", arrayOf("${Config.LYRICS_WEB_URL}?apiKey=$token")) + } + } +} diff --git a/music/lyrics/src/main/kotlin/LyricsCommand.kt b/music/lyrics/src/main/kotlin/LyricsCommand.kt new file mode 100644 index 000000000..3a7624842 --- /dev/null +++ b/music/lyrics/src/main/kotlin/LyricsCommand.kt @@ -0,0 +1,74 @@ +package dev.schlaubi.mikmusic.lyrics + +import com.kotlindiscord.kord.extensions.commands.Arguments +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString +import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand +import dev.schlaubi.lavakord.plugins.lyrics.rest.requestLyrics +import dev.schlaubi.lavakord.plugins.lyrics.rest.searchLyrics +import dev.schlaubi.lyrics.protocol.TimedLyrics +import dev.schlaubi.mikbot.plugin.api.util.discordError +import dev.schlaubi.mikbot.plugin.api.util.forList +import dev.schlaubi.mikmusic.checks.musicQuizAntiCheat +import dev.schlaubi.mikmusic.util.musicModule +import dev.schlaubi.stdx.core.paginate + +class LyricsArguments : Arguments() { + val name by optionalString { + name = "song_name" + description = "commands.lyrics.arguments.song_name.description" + } +} + +suspend fun Extension.lyricsCommand() = publicSlashCommand(::LyricsArguments) { + name = "lyrics" + description = "commands.lyrics.description" + + check { + musicQuizAntiCheat(musicModule) + } + + action { + val player = with(musicModule) { player } + val link = with(musicModule) { link } + + val query = arguments.name ?: player.playingTrack?.info?.title + ?.replace("\\(.+(?!\\))".toRegex(), "") // replace anything in brackets like (official music video) + + if (query == null) { + respond { + content = translate("command.lyrics.no_song_playing") + } + + return@action + } + val lyrics = if (player.playingTrack != null) { + player.requestLyrics() + } else { + val (videoId) = link.node.searchLyrics(query).firstOrNull() + ?: discordError(translate("command.lyrics.no_lyrics")) + link.node.requestLyrics(videoId) + } + + val lines = if (lyrics is TimedLyrics) { + lyrics.lines.map { + if (player.position in it.range) { + "**__${it.line}__**" + } else { + it.line + } + } + } else { + lyrics.text.lines() + } + val paged = lines.paginate(2000) + + editingPaginator { + forList(user, paged, { it }, { _, _ -> lyrics.track.title }) { + footer { + text = translate("command.lyrics.source", arrayOf(lyrics.source)) + } + } + }.send() + } +} diff --git a/music/lyrics/src/main/kotlin/LyricsPlugin.kt b/music/lyrics/src/main/kotlin/LyricsPlugin.kt new file mode 100644 index 000000000..362b952f6 --- /dev/null +++ b/music/lyrics/src/main/kotlin/LyricsPlugin.kt @@ -0,0 +1,24 @@ +package dev.schlaubi.mikmusic.lyrics + +import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder +import dev.schlaubi.mikbot.plugin.api.Plugin +import dev.schlaubi.mikbot.plugin.api.PluginContext +import dev.schlaubi.mikbot.plugin.api.PluginMain +import dev.schlaubi.mikbot.plugin.api.module.MikBotModule + +@PluginMain +class LyricsPlugin(context: PluginContext) : Plugin(context) { + override fun ExtensibleBotBuilder.ExtensionsBuilder.addExtensions() { + add(::LyricsModule) + } +} + +class LyricsModule(context: PluginContext) : MikBotModule(context) { + override val name: String = "lyrics" + override val bundle: String = "lyrics" + + override suspend fun setup() { + lyricsCommand() + karaokeCommand() + } +} diff --git a/music/lyrics/src/main/kotlin/events/Events.kt b/music/lyrics/src/main/kotlin/events/Events.kt new file mode 100644 index 000000000..34e81a3d6 --- /dev/null +++ b/music/lyrics/src/main/kotlin/events/Events.kt @@ -0,0 +1,46 @@ +package dev.schlaubi.mikmusic.lyrics.events + +import kotlinx.datetime.Instant +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator + +@OptIn(ExperimentalSerializationApi::class) +@Serializable +@JsonClassDiscriminator("type") +sealed interface Event + +@SerialName("player_update") +@Serializable +data class PlayerStateUpdateEvent( + val playing: Boolean, + val position: Long, + val timestamp: Instant +) : Event { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerStateUpdateEvent + + if (playing != other.playing) return false + if (position != other.position) return false + + return true + } + + override fun hashCode(): Int { + var result = playing.hashCode() + result = 31 * result + position.hashCode() + return result + } +} + +@SerialName("player_stopped") +@Serializable +data object PlayerStoppedEvent : Event + +@SerialName("next_track") +@Serializable +data class NextTrackEvent(val startPosition: Long) : Event diff --git a/music/lyrics/src/main/resources/translations/lyrics/strings_de_DE.properties b/music/lyrics/src/main/resources/translations/lyrics/strings_de_DE.properties new file mode 100644 index 000000000..8781a4061 --- /dev/null +++ b/music/lyrics/src/main/resources/translations/lyrics/strings_de_DE.properties @@ -0,0 +1,4 @@ +command.lyrics.source=Quelle: {0} +commands.karaoke.description=Lässt dich eine Karaoke party haben +commands.karaoke.not_available=Wir haben keine Karaoke fähigen Songtextre für dieses Lied :( +commands.karaoke.success=Bitte öffne [diese Website]({0}) um Karaoke spaß zu haben diff --git a/music/lyrics/src/main/resources/translations/lyrics/strings_en_GB.properties b/music/lyrics/src/main/resources/translations/lyrics/strings_en_GB.properties new file mode 100644 index 000000000..175d51829 --- /dev/null +++ b/music/lyrics/src/main/resources/translations/lyrics/strings_en_GB.properties @@ -0,0 +1,4 @@ +command.lyrics.source=Source: {0} +commands.karaoke.description=Allows having a karaoke party +commands.karaoke.not_available=We do not have karaoke-capable lyrics for this track :( +commands.karaoke.success=Please open [this website]({0}) to have karaoke fun diff --git a/music/player/build.gradle.kts b/music/player/build.gradle.kts index bb2a5d7da..81b4d622f 100644 --- a/music/player/build.gradle.kts +++ b/music/player/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api(libs.lavakord.sponsorblock) api(libs.lavakord.lavsrc) api(libs.lavakord.lavasearch) + api(libs.lavakord.lyrics) // Plattform support implementation(libs.google.apis.youtube) diff --git a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/autocomplete/AutoCompleteArgument.kt b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/autocomplete/AutoCompleteArgument.kt index 4da4f28b5..37d7d072b 100644 --- a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/autocomplete/AutoCompleteArgument.kt +++ b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/autocomplete/AutoCompleteArgument.kt @@ -36,7 +36,7 @@ fun Arguments.autoCompletedYouTubeQuery(description: String): SingleConverter<St choice(text, text) } result.artists.take(5).forEach { playlist -> - choice("${Emojis.man} ${playlist.info.name}", playlist.lavaSrcInfo.url) + choice("${Emojis.man} ${playlist.info.name}", playlist.lavaSrcInfo.url.toString()) } result.tracks.take(5).forEach { track -> val uri = track.info.uri ?: return@forEach @@ -44,10 +44,10 @@ fun Arguments.autoCompletedYouTubeQuery(description: String): SingleConverter<St } result.albums.take(5).forEach { playlist -> val albumInfo = playlist.lavaSrcInfo - choice("${Emojis.cd} ${albumInfo.author} - ${playlist.info.name}", albumInfo.url) + choice("${Emojis.cd} ${albumInfo.author} - ${playlist.info.name}", albumInfo.url.toString()) } result.playlists.take(5).forEach { playlist -> - choice("${Emojis.scroll} ${playlist.info.name}", playlist.lavaSrcInfo.url) + choice("${Emojis.scroll} ${playlist.info.name}", playlist.lavaSrcInfo.url.toString()) } } } diff --git a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/QueueResponse.kt b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/QueueResponse.kt index 61e23f0a6..2e4056bff 100644 --- a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/QueueResponse.kt +++ b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/QueueResponse.kt @@ -44,7 +44,7 @@ class Playlist(private val playlist: Playlist, tracks: List<Track>) : QueueSearc if (lavaSrcInfo != null) { url = lavaSrcInfo.url thumbnail { - url = lavaSrcInfo.artworkUrl + url = lavaSrcInfo.artworkUrl.toString() } author { diff --git a/runtime/plugins.txt b/runtime/plugins.txt index 6e050c8b3..364f366d8 100644 --- a/runtime/plugins.txt +++ b/runtime/plugins.txt @@ -1,4 +1,4 @@ -:core:gdpr -:core:redeploy-hook +:core:ktor :music:player :music:commands +:music:lyrics diff --git a/settings.gradle.kts b/settings.gradle.kts index 52a912810..df88ae578 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ if (System.getenv("BUILD_PLUGIN_CI")?.toBoolean() != true) { ":music", "music:player", "music:commands", + "music:lyrics", "clients:discord-oauth", "clients:haste-client", "clients:image-color-client",