diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 53bf3196..6d0ee1c2 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ef43e0e..f4f463fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] -kotlin = "2.0.0-RC2" +kotlin = "2.0.0" kordex = "1.8.0-SNAPSHOT" kmongo = "4.11.0" -coroutines = "1.8.0" +coroutines = "1.8.1" serialization = "1.6.3" -ktor = "2.3.10" -kord = "0.13.1" -api = "3.31.0" -ksp = "2.0.0-RC2-1.0.20" +ktor = "2.3.11" +kord = "0.14.0-SNAPSHOT" +api = "3.32.0" +ksp = "2.0.0-1.0.21" lavakord = "6.2.0" [libraries] diff --git a/music/build.gradle.kts b/music/build.gradle.kts index 89427517..068c9bae 100644 --- a/music/build.gradle.kts +++ b/music/build.gradle.kts @@ -1,3 +1,3 @@ subprojects { - version = "3.6.1-SNAPSHOT" + version = "3.7.0-SNAPSHOT" } diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/context/PlayMessageAction.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/context/PlayMessageAction.kt index f0e40c14..fca1c625 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/context/PlayMessageAction.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/context/PlayMessageAction.kt @@ -31,6 +31,10 @@ class PlayMessageActionArguments(override val query: String) : QueueOptions { override val force: Boolean = false override val top: Boolean = false override val searchProvider: QueueOptions.SearchProvider? = null + override val shuffle: Boolean? = null + override val loop: Boolean? = null + override val loopQueue: Boolean? = null + } private suspend fun EphemeralMessageCommandContext<*>.queue( diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/AddCommand.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/AddCommand.kt index 12b41119..424be9bc 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/AddCommand.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/AddCommand.kt @@ -23,6 +23,10 @@ class PlaylistAddArguments : PlaylistArguments(), QueueOptions { } override val top: Boolean = false override val force: Boolean = false + override val shuffle: Boolean? = null + override val loop: Boolean? = null + override val loopQueue: Boolean? = null + } fun PlaylistModule.addCommand() = ephemeralSubCommand(::PlaylistAddArguments) { diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/Common.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/Common.kt index 86487958..44e1d705 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/Common.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/Common.kt @@ -21,49 +21,57 @@ import org.litote.kmongo.eq import org.litote.kmongo.or import org.litote.kmongo.util.KMongoUtil -abstract class PlaylistArguments(val onlyMine:Boolean = true) : Arguments() { - val name by string { - name = "name" - description = "The name of the playlist" +interface PlaylistOptions { + val name: String +} - validate { - getPlaylistOrNull(context.getUser()!!, value) ?: context.notFound(value) - } +abstract class PlaylistArguments(onlyMine: Boolean = true) : Arguments(), PlaylistOptions { + override val name by playlistName(onlyMine) +} - autoComplete { - val genericFilter = if (onlyMine) { - Playlist::authorId eq user.id - } else { - or(Playlist::public eq true, Playlist::authorId eq user.id) - } - val input = focusedOption.value - val names = PlaylistDatabase.collection.find( - and(genericFilter, - KMongoUtil.toBson("{name: /$input/i}")) - ).toFlow() - .take(25) - .toList() - suggestString { - names.forEach { choice(it.name, it.name) } - } - } - } +fun Arguments.playlistName(onlyMine: Boolean) = string { + name = "name" + description = "The name of the playlist" - private suspend fun CommandContext.notFound(value: String): Nothing { - throw DiscordRelayedException(translate("command.playlist.unknown_playlist", arrayOf(value))) + validate { + getPlaylistOrNull(context.getUser()!!, value) ?: context.notFound(value) } - suspend fun getPlaylistOrNull(userBehavior: UserBehavior, name: String) = - PlaylistDatabase.collection.findOne( + autoComplete { + val genericFilter = if (onlyMine) { + Playlist::authorId eq user.id + } else { + or(Playlist::public eq true, Playlist::authorId eq user.id) + } + val input = focusedOption.value + val names = PlaylistDatabase.collection.find( and( - Playlist::name eq name, - or(Playlist::public eq true, Playlist::authorId eq userBehavior.id) + genericFilter, + KMongoUtil.toBson("{name: /$input/i}") ) - ) + ).toFlow() + .take(25) + .toList() + suggestString { + names.forEach { choice(it.name, it.name) } + } + } } -suspend fun EphemeralSlashCommandContext.getPlaylist() = - arguments.getPlaylistOrNull(user, arguments.name) ?: error("Could not load playlist") +private suspend fun CommandContext.notFound(value: String): Nothing { + throw DiscordRelayedException(translate("command.playlist.unknown_playlist", arrayOf(value))) +} + +private suspend fun getPlaylistOrNull(userBehavior: UserBehavior, name: String) = + PlaylistDatabase.collection.findOne( + and( + Playlist::name eq name, + or(Playlist::public eq true, Playlist::authorId eq userBehavior.id) + ) + ) +suspend fun EphemeralSlashCommandContext.getPlaylist() + where T : Arguments, T : PlaylistOptions = + getPlaylistOrNull(user, arguments.name) ?: error("Could not load playlist") class PlaylistModule(context: PluginContext) : SubCommandModule(context) { diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/LoadCommand.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/LoadCommand.kt index 20b16e89..5bba677b 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/LoadCommand.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/LoadCommand.kt @@ -1,10 +1,13 @@ package dev.schlaubi.mikmusic.playlist.commands import dev.schlaubi.mikmusic.checks.joinSameChannelCheck +import dev.schlaubi.mikmusic.player.queue.SchedulingArguments import dev.schlaubi.mikmusic.playlist.PlaylistDatabase import dev.schlaubi.mikmusic.util.mapToQueuedTrack -class LoadArguments : PlaylistArguments(onlyMine = false) +class LoadArguments : SchedulingArguments(), PlaylistOptions { + override val name by playlistName(onlyMine = false) +} fun PlaylistModule.loadCommand() = ephemeralSubCommand(::LoadArguments) { name = "load" @@ -20,7 +23,8 @@ fun PlaylistModule.loadCommand() = ephemeralSubCommand(::LoadArguments) { musicPlayer.queueTrack( force = false, onTop = false, - tracks = playlist.getTracks(musicPlayer.node).mapToQueuedTrack(user) + tracks = playlist.getTracks(musicPlayer.node).mapToQueuedTrack(user), + schedulingOptions = arguments ) respond { diff --git a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/SaveCommand.kt b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/SaveCommand.kt index bd3b5b4a..7595848d 100644 --- a/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/SaveCommand.kt +++ b/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/SaveCommand.kt @@ -31,6 +31,9 @@ class PlaylistSaveArguments : Arguments(), QueueOptions { override val force: Boolean = false override val top: Boolean = false override val searchProvider: QueueOptions.SearchProvider? = null + override val shuffle: Boolean? = null + override val loop: Boolean? = null + override val loopQueue: Boolean? = null } fun PlaylistModule.saveCommand() = ephemeralSubCommand(::PlaylistSaveArguments) { diff --git a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/MusicPlayer.kt b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/MusicPlayer.kt index c4b68743..182826cb 100644 --- a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/MusicPlayer.kt +++ b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/MusicPlayer.kt @@ -27,6 +27,7 @@ import dev.schlaubi.lavakord.rest.getPlayer import dev.schlaubi.lavakord.rest.updatePlayer import dev.schlaubi.mikmusic.core.settings.MusicSettingsDatabase import dev.schlaubi.mikmusic.musicchannel.updateMessage +import dev.schlaubi.mikmusic.player.queue.SchedulingOptions import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -147,11 +148,16 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by li onTop: Boolean, tracks: Collection, position: Duration? = null, + schedulingOptions: SchedulingOptions? = null ) = lock.withLock { val isFirst = nextSongIsFirst require(isFirst || position == null) { "Can only specify position if nextSong is first" } queue.addTracks(tracks, onTop || force) + queue.shuffle = schedulingOptions?.shuffle ?: queue.shuffle + loopQueue = schedulingOptions?.loopQueue ?: loopQueue + repeat = schedulingOptions?.loop ?: repeat + if ((force || isFirst) && !dontQueue) { startNextSong(position = position) waitForPlayerUpdate() @@ -310,14 +316,14 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by li // called under lock private suspend fun startNextSong(lastSong: Track? = null, position: Duration? = null) { updateSponsorBlock() - val nextTrack: QueuedTrack? = when { - lastSong != null && repeat -> playingTrack!! - else -> queue.poll() - } - if (nextTrack == null) { + if (queue.isEmpty()) { updateMusicChannelMessage() return } + val nextTrack: QueuedTrack = when { + lastSong != null && repeat -> playingTrack!! + else -> queue.poll() + } playingTrack = nextTrack link.player.playTrack(nextTrack.track) { diff --git a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/TrackFinder.kt b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/TrackFinder.kt index a9fbf741..22699ffd 100644 --- a/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/TrackFinder.kt +++ b/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/TrackFinder.kt @@ -6,6 +6,7 @@ import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSla import com.kotlindiscord.kord.extensions.commands.application.slash.converters.ChoiceEnum import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.optionalEnumChoice import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalBoolean import dev.arbjerg.lavalink.protocol.v4.Exception import dev.arbjerg.lavalink.protocol.v4.LoadResult import dev.kord.rest.builder.message.embed @@ -22,7 +23,13 @@ private val LOG = KotlinLogging.logger { } private val urlProtocol = "^https?://".toRegex() -interface QueueOptions { +interface SchedulingOptions { + val shuffle: Boolean? + val loop: Boolean? + val loopQueue: Boolean? +} + +interface QueueOptions : SchedulingOptions { val query: String val force: Boolean val top: Boolean @@ -34,7 +41,24 @@ interface QueueOptions { } } -abstract class QueueArguments : Arguments(), QueueOptions { +abstract class SchedulingArguments : Arguments(), SchedulingOptions { + override val shuffle: Boolean? by optionalBoolean { + name = "shuffle" + description = "scheduler.options.shuffle.description" + } + + override val loop: Boolean? by optionalBoolean { + name = "loop" + description = "scheduler.options.loop.description" + } + + override val loopQueue: Boolean? by optionalBoolean { + name = "loop-queue" + description = "scheduler.options.loop_queue.description" + } +} + +abstract class QueueArguments : SchedulingArguments(), QueueOptions { override val query by autoCompletedYouTubeQuery("The query to play") override val force by defaultingBoolean { name = "force" @@ -167,7 +191,8 @@ suspend fun CommandContext.queueTracks( musicPlayer.queueTrack( arguments.force, arguments.top, - searchResult.tracks.map { SimpleQueuedTrack(it, getUser()!!.id) } + searchResult.tracks.map { SimpleQueuedTrack(it, getUser()!!.id) }, + schedulingOptions = arguments ) } }