Skip to content

Commit

Permalink
Add AutoPlay again
Browse files Browse the repository at this point in the history
  • Loading branch information
DRSchlaubi committed Sep 28, 2024
1 parent 73361da commit 887df06
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 88 deletions.
2 changes: 1 addition & 1 deletion music/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
subprojects {
version = "3.10.0-SNAPSHOT"
version = "3.11.0-SNAPSHOT"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ suspend fun MusicModule.commands() {
clearCommand()
fixCommand()
nextCommand()
radioCommand()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.schlaubi.mikmusic.commands

import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand
import dev.kord.rest.builder.message.embed
import dev.schlaubi.mikbot.plugin.api.util.forList
import dev.schlaubi.mikmusic.checks.anyMusicPlaying
import dev.schlaubi.mikmusic.core.MusicModule
Expand All @@ -20,11 +21,14 @@ suspend fun MusicModule.queueCommand() = ephemeralSlashCommand {
}

action {
if (musicPlayer.queuedTracks.isEmpty()) {
if (musicPlayer.queue.isEmpty()) {
val track = musicPlayer.playingTrack
if (track != null) {
respond {
content = translate("commands.queue.now_playing", arrayOf(track.format()))
embed {
musicPlayer.addAutoPlaySongs(::translate)
description = translate("commands.queue.now_playing", arrayOf(track.format()))
}
}
} else {
respond {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package dev.schlaubi.mikmusic.commands

import com.github.topi314.lavasrc.protocol.ExtendedPlaylistInfo
import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand
import dev.arbjerg.lavalink.protocol.v4.LoadResult
import dev.schlaubi.lavakord.plugins.lavasearch.model.SearchType
import dev.schlaubi.lavakord.plugins.lavasrc.lavaSrcInfo
import dev.schlaubi.lavakord.rest.loadItem
import dev.schlaubi.mikbot.plugin.api.util.discordError
import dev.schlaubi.mikmusic.autocomplete.autoCompletedYouTubeQuery
import dev.schlaubi.mikmusic.checks.joinSameChannelCheck
import dev.schlaubi.mikmusic.core.MusicModule
import dev.schlaubi.mikmusic.player.SimpleQueuedTrack
import dev.schlaubi.mikmusic.player.enableAutoPlay
import dev.schlaubi.mikmusic.util.spotifyId

class RadioArguments : Arguments() {
val query by string {
name = "query"
description = "commands.radio.arguments.query.description"
}
val query by autoCompletedYouTubeQuery(
"commands.radio.arguments.query.description",
SearchType.Artist, SearchType.Track
)
}

/*
suspend fun MusicModule.radioCommand() {
ephemeralSlashCommand(::RadioArguments) {
name = "radio"
Expand All @@ -21,20 +32,31 @@ suspend fun MusicModule.radioCommand() {
}

action {
val results = musicPlayer.loadItem("ytsearch: ${arguments.query}")
val track = results.tracks.firstOrNull()?.toTrack()
val videoId = track?.youtubeId
if (track == null || videoId == null) {
discordError(translate("commands.radio.invalid_vide"))
if (musicPlayer.hasAutoPlay) {
discordError(translate("commands.radio.already_enabled"))
}
val seedItem = (musicPlayer.loadItem(arguments.query))

val (item, isTrack) = when (seedItem) {
is LoadResult.TrackLoaded -> seedItem.data to true
is LoadResult.SearchResult -> seedItem.data.tracks.first() to true
is LoadResult.PlaylistLoaded -> seedItem.data.tracks.first()
.takeIf { seedItem.data.lavaSrcInfo.type == ExtendedPlaylistInfo.Type.ARTIST }
?.let { it to false }

musicPlayer.enableAutoPlay(videoId, radioParam)
musicPlayer.queueTrack(force = false, onTop = false, tracks = listOf(SimpleQueuedTrack(track, user.id)))
else -> discordError(translate("commands.radio.no_matching_songs"))
} ?: discordError(translate("commands.radio.no_matching_songs"))

if (isTrack) {
musicPlayer.enableAutoPlay(seedTracks = listOf(item.spotifyId!!))
} else {
musicPlayer.enableAutoPlay(seedArtists = listOf(item.spotifyId!!))
}

musicPlayer.queueTrack(false, false, tracks = listOf(SimpleQueuedTrack(item, user.id)))
respond {
content = translate("commands.radio.queued")
}
}
}
}
*/
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.kotlindiscord.kord.extensions.koin.KordExContext
import dev.kord.core.behavior.interaction.suggestString
import dev.kord.core.event.interaction.GuildAutoCompleteInteractionCreateEvent
import dev.kord.x.emoji.Emojis
import dev.schlaubi.lavakord.plugins.lavasearch.model.SearchType
import dev.schlaubi.lavakord.plugins.lavasearch.rest.search
import dev.schlaubi.lavakord.plugins.lavasrc.lavaSrcInfo
import dev.schlaubi.mikmusic.core.Config
Expand All @@ -19,7 +20,7 @@ private val musicModule = KordExContext.get().get<ExtensibleBot>()
/**
* Creates a `query` argument with [description] supporting YouTube Auto-complete.
*/
fun Arguments.autoCompletedYouTubeQuery(description: String): SingleConverter<String> = string {
fun Arguments.autoCompletedYouTubeQuery(description: String, vararg searchTypes: SearchType): SingleConverter<String> = string {
name = AUTOCOMPLETE_QUERY_OPTION
this.description = description

Expand All @@ -29,7 +30,7 @@ fun Arguments.autoCompletedYouTubeQuery(description: String): SingleConverter<St
if (input.isNotBlank()) {
val result = musicModule
.getMusicPlayer((it as GuildAutoCompleteInteractionCreateEvent).interaction.guild)
.search("${Config.DEFAULT_SEARCH_PROVIDER}:$input")
.search("${Config.DEFAULT_SEARCH_PROVIDER}:$input", *searchTypes)

suggestString {
result.texts.take(5).map { (text) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,12 @@ suspend fun updateMessage(
additionalCondition = !playingQueueTrack.isOnLast
)
}
// Disabled due to YouTube changing the underlying API
// musicButton(
// musicPlayer,
// autoPlay,
// Emojis.blueCar,
// enabled = musicPlayer.autoPlay != null
// )
musicButton(
musicPlayer,
autoPlay,
Emojis.blueCar,
enabled = musicPlayer.autoPlay != null
)
}
actionRow {
musicButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ class MusicInteractionModule(context: PluginContext) : MikBotModule(context) {

autoPlay -> {
if (player.autoPlay == null) {
player.enableAutoPlay()
try {
player.enableAutoPlay()
} catch (_: IllegalStateException) {
discordError(translate("commands.radio.no_matching_songs"))
}
} else {
player.resetAutoPlay()
}
Expand Down
101 changes: 44 additions & 57 deletions music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/AutoPlay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@ import dev.arbjerg.lavalink.protocol.v4.Track
import dev.kord.rest.builder.message.EmbedBuilder
import dev.schlaubi.lavakord.rest.loadItem
import dev.schlaubi.mikbot.plugin.api.util.Translator
import dev.schlaubi.mikmusic.innerttube.InnerTubeClient
import dev.schlaubi.mikmusic.innerttube.Text
import dev.schlaubi.mikmusic.util.LinkedListSerializer
import dev.schlaubi.mikmusic.util.youtubeId
import dev.schlaubi.mikmusic.util.format
import dev.schlaubi.mikmusic.util.spotifyId
import kotlinx.serialization.Serializable
import java.util.*

@Serializable
data class AutoPlayContext(
val playlist: String?,
val params: String?,
val seedGenres: List<String>,
val seedArtists: List<String>,
val history: List<Track>,
@Serializable(with = LinkedListSerializer::class) val songs: LinkedList<Track> = LinkedList(),
) {
val initialSize = songs.size

@Serializable
data class Track(val name: String, val artists: List<String>, val id: String)
companion object {
val EMPTY = AutoPlayContext(emptyList(), emptyList(), emptyList())
}
}


Expand All @@ -34,36 +35,40 @@ suspend fun MusicPlayer.resetAutoPlay() {
updateMusicChannelMessage()
}

suspend fun MusicPlayer.enableAutoPlay(videoId: String? = null, params: String? = null) {
val realVideoId = videoId ?: playingTrack?.track?.youtubeId ?: return
fetchAutoPlay(realVideoId, params = params)
suspend fun MusicPlayer.enableAutoPlay() {
val seedTracks = queue.tracks.mapNotNull { it.track.spotifyId }
if (seedTracks.isEmpty()) error("Spotify tracks are required")
fetchAutoPlay(seedTracks = seedTracks)
}

private suspend fun MusicPlayer.fetchAutoPlay(songId: String, playlistId: String? = null, params: String? = null) {
val response = InnerTubeClient.requestNextSongs(songId, playlistId, params)
val songsTab = response.contents
.singleColumnMusicWatchNextResultsRenderer
.tabbedRenderer
.watchNextTabbedResultsRenderer
.tabs.firstOrNull { it.tabRenderer.content?.musicQueueRenderer != null } ?: return
val songRenderers = (songsTab
.tabRenderer
.content
?.musicQueueRenderer ?: return)
.content
.playlistPanelRenderer
.contents
.mapNotNull { it.playlistPanelVideoWrapperRenderer?.primaryRenderer }
val newPlayListId = songRenderers.firstOrNull()?.navigationEndpoints?.watchEndpoint?.playlistId
val songs = songRenderers
.drop(1) // First song is requested song
.map {
val name = it.title.runs.joinToString(" ", transform = Text::text)
AutoPlayContext.Track(
name, it.longByLineText?.runs?.map(Text::text)?.take(1) ?: emptyList(), it.videoId
)
suspend fun MusicPlayer.enableAutoPlay(
seedTracks: List<String> = emptyList(),
seedArtists: List<String> = emptyList(),
seedGenres: List<String> = emptyList(),
) {
autoPlay = AutoPlayContext(seedGenres, seedArtists, emptyList())
fetchAutoPlay(seedTracks = seedTracks)
}

private suspend fun MusicPlayer.fetchAutoPlay(
seedTracks: List<String> = emptyList(),
seedArtists: List<String> = emptyList(),
seedGenres: List<String> = emptyList(),
) {
val query = buildString {
append("sprec:")
if (seedTracks.isNotEmpty()) {
append("seed_tracks=${seedTracks.joinToString(",")}")
}
autoPlay = AutoPlayContext(newPlayListId, autoPlay?.params, LinkedList(songs))
if (seedArtists.isNotEmpty()) {
append("seed_artists=${seedTracks.joinToString(",")}")
}
if (seedGenres.isNotEmpty()) {
append("seed_genres=${seedTracks.joinToString(",")}")
}
}
val (_, list) = link.loadItem(query) as LoadResult.PlaylistLoaded
autoPlay = (autoPlay ?: AutoPlayContext.EMPTY).copy(history = list.tracks, songs = LinkedList(list.tracks))
}

context(EmbedBuilder)
Expand All @@ -72,35 +77,17 @@ suspend fun MusicPlayer.addAutoPlaySongs(translate: Translator) {
if (!songs.isNullOrEmpty()) {
field {
name = translate("music.auto_play.next_song", "music")
value = songs.joinToString("\n") {
buildString {
append(it.name)
if (it.artists.isNotEmpty()) {
append(" - ")
append(it.artists.joinToString(", "))
}
}
}
value = songs.joinToString<Track>("\n", transform = Track::format)
}
}
}

suspend fun MusicPlayer.findNextAutoPlayedSong(lastSong: Track?): Track? {
suspend fun MusicPlayer.findNextAutoPlayedSong(): Track? {
val currentAutoPlay = autoPlay ?: return null
if (currentAutoPlay.songs.isNotEmpty()) {
return currentAutoPlay.songs.poll().fetchTrack()
return currentAutoPlay.songs.poll()
}
val track = lastSong?.youtubeId ?: return null
fetchAutoPlay(track, autoPlay?.playlist, autoPlay?.params)
return autoPlay?.songs?.poll()?.fetchTrack()
fetchAutoPlay(currentAutoPlay.history.mapNotNull(Track::spotifyId))
return autoPlay?.songs?.poll()
}

context(MusicPlayer)
private suspend fun AutoPlayContext.Track.fetchTrack(): Track? {
val response = link.loadItem("https://www.youtube.com/watch?v=$id")
return if (response is LoadResult.TrackLoaded) {
response.data
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import dev.arbjerg.lavalink.protocol.v4.PlayerUpdate
import dev.arbjerg.lavalink.protocol.v4.Track
import dev.arbjerg.lavalink.protocol.v4.toOmissible
import dev.kord.common.entity.Snowflake
import dev.kord.core.Kord
import dev.kord.core.KordObject
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.entity.channel.VoiceChannel
import dev.schlaubi.lavakord.UnsafeRestApi
Expand Down Expand Up @@ -48,7 +50,7 @@ internal data class SavedTrack(
val pause: Boolean,
)

class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by link, KordExKoinComponent {
class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by link, KordExKoinComponent , KordObject {

private val lock = Mutex()

Expand All @@ -66,6 +68,10 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by li
internal var autoPlay: AutoPlayContext? = null
internal var savedTrack: SavedTrack? = null
private var dontQueue = false
override val kord: Kord
get() = guild.kord
val hasAutoPlay: Boolean
get() = !autoPlay?.songs.isNullOrEmpty()

init {
guild.kord.launch {
Expand Down Expand Up @@ -248,7 +254,7 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by li
return@onTrackEnd
}
if ((!repeat && !loopQueue && queue.isEmpty()) && event.reason != Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason.REPLACED) {
val autoPlayTrack = findNextAutoPlayedSong(event.track)
val autoPlayTrack = findNextAutoPlayedSong()
if (autoPlayTrack != null) {
queue.addTracks(SimpleQueuedTrack(autoPlayTrack, guild.kord.selfId))
} else {
Expand Down Expand Up @@ -341,6 +347,12 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) : Link by li
updateMusicChannelMessage()
}

suspend fun start() {
if (playingTrack == null) {
startNextSong()
}
}

suspend fun stop() {
player.stopTrack()
link.disconnectAudio()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dev.schlaubi.mikmusic.util

import dev.arbjerg.lavalink.protocol.v4.Track

val Track.spotifyId: String?
get() = if(info.sourceName == "spotify") info.identifier else null
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ commands.volume.current=Die derzeitige Lautstärke ist `{0}`
commands.skip.skipped=Song wurde übersprungen
music.queue.search.title=Suchergebnisse {0}/{1}
commands.queue.no_songs=Derzeit sind keine Lieder in der Warteschlange oder am Spielen.
commands.queue.now_playing=Derzeit spielt: `{0}`.
commands.queue.now_playing=Derzeit spielt: {0}.
music.multiple_scheduler_options=Du kannst nur eins von 'shuffle', 'repeat' oder 'loopqueue' auswählen. Willst du die derzeitige Einstellung überschreiben?
music.general.aborted=Prozess abgebrochen!
commands.repeat.enabled=Das derzeitige Lied wird nun wiederholt! (Wird das nicht etwas eintönig?)
Expand Down Expand Up @@ -167,3 +167,5 @@ scheduler.options.loop_queue.description=Ob die Warteschlange nach diesem Elemen
queue.options.force.description=Lässt dieses Element die eingereihten Tracks überspringen
queue.options.top.description=Fügt dieses Element oben in die Warteschlange ein
queue.options.search_provider.description=Welcher Suchanbieter verwendet werden soll
commands.radio.already_enabled=AutoPlay ist bereits aktiviert, bitte stoppe den Bot um es zu deaktivieren
commands.radio.no_matching_songs=Es konten keine Lieder gefunden werden, welche mit AutoPlay kompatibel sind
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ commands.volume.current=The current volume is `{0}`
commands.skip.skipped=Song got skipped
music.queue.search.title=Search results {0}/{1}
commands.queue.no_songs=There are currently no songs queued or playing.
commands.queue.now_playing=Now playing: `{0}`.
commands.queue.now_playing=Now playing: {0}.
music.multiple_scheduler_options=You can only enable either shuffle, repeat or loopqueue. Do you want to overwrite these settings?
music.general.aborted=Process aborted!
commands.repeat.enabled=The current song is now on repeat!
Expand Down Expand Up @@ -172,3 +172,5 @@ scheduler.options.loop_queue.description=Whether to loop the queue after this
queue.options.force.description=Makes this item skip the queued tracks
queue.options.top.description=Adds this item to the top of the queueTracks
queue.options.search_provider.description=Which searchprovider to use
commands.radio.already_enabled=AutoPlay is already activated, please stop the bot to deactivate it
commands.radio.no_matching_songs=No AutoPlay-compatible songs were found, please try again
Loading

0 comments on commit 887df06

Please sign in to comment.