Skip to content

Commit

Permalink
Allow setting user data on tracks (lavalink-devs#983)
Browse files Browse the repository at this point in the history
* implement track user data

* fix unit tests and add Track#copyWithUserData

* remove unnecessary exceptionally

* use loadItemSync instead of loadItem in player rest handler

* Update protocol/src/commonTest/kotlin/PlayerSerializerTest.kt

Co-authored-by: Duncan Sterken <contact@duncte123.me>

* throw http 400 if both track and encodedTrack/identifier is set

* add convenient deserializeUserData method for java

---------

Co-authored-by: Duncan Sterken <contact@duncte123.me>
  • Loading branch information
topi314 and duncte123 authored Dec 2, 2023
1 parent c2b6b09 commit 3c442f3
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,28 @@ class PlayerRestHandler(
): ResponseEntity<Player> {
val context = socketContext(socketServer, sessionId)

val encodedTrack = playerUpdate.encodedTrack
if (encodedTrack is Omissible.Present && playerUpdate.identifier is Omissible.Present) {
if (playerUpdate.track.isPresent() && (playerUpdate.encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present)) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot specify both track and encodedTrack/identifier")
}

val track = if (playerUpdate.track.isPresent()) {
playerUpdate.track
} else {
if (playerUpdate.encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present) {
PlayerUpdateTrack(
playerUpdate.encodedTrack,
playerUpdate.identifier
).toOmissible()
} else {
Omissible.Omitted()
}
}

val encodedTrack = track.ifPresent { it.encoded } ?: Omissible.Omitted()
val identifier = track.ifPresent { it.identifier } ?: Omissible.Omitted()
val userData = track.ifPresent { it.userData } ?: Omissible.Omitted()

if (encodedTrack is Omissible.Present && identifier is Omissible.Present) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot specify both encodedTrack and identifier")
}

Expand Down Expand Up @@ -112,25 +132,31 @@ class PlayerRestHandler(

// we handle pause differently for playing new tracks
val paused = playerUpdate.paused
paused.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
paused.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
?.let {
player.setPause(it)
}

// we handle userData differently for playing new tracks
userData.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
?.let {
player.track?.userData = it
}

playerUpdate.volume.ifPresent {
player.setVolume(it)
}

// we handle position differently for playing new tracks
playerUpdate.position.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
playerUpdate.position.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
?.let {
if (player.isPlaying) {
player.seekTo(it)
SocketServer.sendPlayerUpdate(context, player)
}
}

playerUpdate.endTime.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
playerUpdate.endTime.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
?.let { endTime ->
val marker = TrackMarker(endTime, TrackEndMarkerHandler(player))
player.track?.setMarker(marker)
Expand All @@ -141,72 +167,75 @@ class PlayerRestHandler(
SocketServer.sendPlayerUpdate(context, player)
}

if (encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present) {
if (encodedTrack is Omissible.Present || identifier is Omissible.Present) {

if (noReplace && player.track != null) {
log.info("Skipping play request because of noReplace")
return ResponseEntity.ok(player.toPlayer(context, pluginInfoModifiers))
}
player.setPause(if (paused is Omissible.Present) paused.value else false)

val track: AudioTrack? = if (encodedTrack is Omissible.Present) {
val newTrack: AudioTrack? = if (encodedTrack is Omissible.Present) {
encodedTrack.value?.let {
decodeTrack(context.audioPlayerManager, it)
}
} else {
val trackFuture = CompletableFuture<AudioTrack>()
val identifier = playerUpdate.identifier as Omissible.Present
context.audioPlayerManager.loadItem(identifier.value, object : AudioLoadResultHandler {
override fun trackLoaded(track: AudioTrack) {
trackFuture.complete(track)
}

override fun playlistLoaded(playlist: AudioPlaylist) {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Cannot play a playlist or search result"
context.audioPlayerManager.loadItemSync(
(identifier as Omissible.Present).value,
object : AudioLoadResultHandler {
override fun trackLoaded(track: AudioTrack) {
trackFuture.complete(track)
}

override fun playlistLoaded(playlist: AudioPlaylist) {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Cannot play a playlist or search result"
)
)
)
}

override fun noMatches() {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.BAD_REQUEST,
"No matches found for identifier"
}

override fun noMatches() {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.BAD_REQUEST,
"No matches found for identifier"
)
)
)
}

override fun loadFailed(exception: FriendlyException) {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
exception.message,
getRootCause(exception)
}

override fun loadFailed(exception: FriendlyException) {
trackFuture.completeExceptionally(
ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
exception.message,
getRootCause(exception)
)
)
)
}
})
}
})

trackFuture.exceptionally {
throw it
}.join()
trackFuture.join()
}

track?.let {
newTrack?.let {
playerUpdate.position.ifPresent { position ->
track.position = position
newTrack.position = position
}

userData.ifPresent { userData ->
newTrack.userData = userData
}

playerUpdate.endTime.ifPresent { endTime ->
if (endTime != null) {
track.setMarker(TrackMarker(endTime, TrackEndMarkerHandler(player)))
newTrack.setMarker(TrackMarker(endTime, TrackEndMarkerHandler(player)))
}
}

player.play(track)
player.play(newTrack)
player.provideTo(context.getMediaConnection(player))
} ?: player.stop()
}
Expand Down
2 changes: 1 addition & 1 deletion LavalinkServer/src/main/java/lavalink/server/util/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fun AudioTrack.toTrack(encoded: String, pluginInfoModifiers: List<AudioPluginInf
acc + jsonObject
}

return Track(encoded, this.toInfo(), pluginInfo)
return Track(encoded, this.toInfo(), pluginInfo, this.userData as? JsonObject ?: JsonObject(emptyMap()))
}

private operator fun JsonObject.plus(other: JsonObject) = JsonObject(toMap() + other.toMap())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ data class Player(
data class Track(
val encoded: String,
val info: TrackInfo,
val pluginInfo: JsonObject
val pluginInfo: JsonObject,
val userData: JsonObject
) : LoadResult.Data {

/**
Expand All @@ -44,6 +45,28 @@ data class Track(
* @return the deserialized plugin info as type T
*/
fun <T> deserializePluginInfo(deserializer: DeserializationStrategy<T>): T = pluginInfo.deserialize(deserializer)

/**
* Deserialize the user data into a specific type.
* This method is a convenience method meant to be used in Java,
* since Kotlin extension methods are painful to use in Java.
*
* @param deserializer The deserializer to use. (e.g. `T.Companion.serializer()`)
*
* @return the deserialized user data as type T
*/
fun <T> deserializeUserData(deserializer: DeserializationStrategy<T>): T = userData.deserialize(deserializer)

/**
* Copy this track with a new user data json.
*
* @param userData The new user data json.
*
* @return A copy of this track with the new user data json.
*/
fun copyWithUserData(userData: JsonObject): Track {
return copy(userData = userData)
}
}

@Serializable
Expand Down Expand Up @@ -84,10 +107,20 @@ data class PlayerState(
val ping: Long
)

@Serializable
data class PlayerUpdateTrack(
val encoded: Omissible<String?> = Omissible.Omitted(),
val identifier: Omissible<String> = Omissible.Omitted(),
val userData: Omissible<JsonObject> = Omissible.Omitted()
)

@Serializable
data class PlayerUpdate(
@Deprecated("Use PlayerUpdateTrack#encoded instead", ReplaceWith("encoded"))
val encodedTrack: Omissible<String?> = Omissible.Omitted(),
@Deprecated("Use PlayerUpdateTrack#identifier instead")
val identifier: Omissible<String> = Omissible.Omitted(),
val track: Omissible<PlayerUpdateTrack> = Omissible.Omitted(),
val position: Omissible<Long> = Omissible.Omitted(),
val endTime: Omissible<Long?> = Omissible.Omitted(),
val volume: Omissible<Int> = Omissible.Omitted(),
Expand Down
3 changes: 2 additions & 1 deletion protocol/src/commonTest/kotlin/LoadResultSerializerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class LoadResultSerializerTest {
"isrc": null,
"sourceName": "youtube"
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
},
"exception": null
}
Expand Down
12 changes: 8 additions & 4 deletions protocol/src/commonTest/kotlin/MessageSerializerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ class MessageSerializerTest {
"isrc": null,
"sourceName": "youtube"
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
}
}
""".trimIndent()
Expand Down Expand Up @@ -183,7 +184,8 @@ class MessageSerializerTest {
"isrc": null,
"sourceName": "youtube"
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
},
"reason": "finished"
}
Expand Down Expand Up @@ -238,7 +240,8 @@ class MessageSerializerTest {
"isrc": null,
"sourceName": "youtube"
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
},
"exception": {
"message": "...",
Expand Down Expand Up @@ -301,7 +304,8 @@ class MessageSerializerTest {
"isrc": null,
"sourceName": "youtube"
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
},
"thresholdMs": 123456789
}
Expand Down
16 changes: 10 additions & 6 deletions protocol/src/commonTest/kotlin/PlayerSerializerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ private const val json = """
"artworkUrl": null,
"isrc": null
},
"pluginInfo": {}
"pluginInfo": {},
"userData": {}
},
"volume": 100,
"paused": false,
Expand All @@ -52,7 +53,9 @@ private const val json = """
//language=json
const val updateJson = """
{
"identifier": "...",
"track": {
"identifier": "..."
},
"endTime": 0,
"volume": 100,
"position": 32400,
Expand Down Expand Up @@ -112,8 +115,7 @@ class PlayerSerializerTest {
val json = """{}"""

test<PlayerUpdate>(json) {
assertIs<Omissible.Omitted<*>>(encodedTrack)
assertIs<Omissible.Omitted<*>>(identifier)
assertIs<Omissible.Omitted<*>>(track)
assertIs<Omissible.Omitted<*>>(position)
assertIs<Omissible.Omitted<*>>(endTime)
assertIs<Omissible.Omitted<*>>(volume)
Expand All @@ -127,7 +129,7 @@ class PlayerSerializerTest {
@JsName("test3")
fun `test encodedTrack and identifier exclusivity`() {
//language=json
val json = """{"encodedTrack": "", "identifier": ""}"""
val json = """{"track": {"encoded": "", "identifier": ""}}"""

assertFailsWith<IllegalArgumentException> { Json.decodeFromString(json) }
}
Expand All @@ -136,7 +138,9 @@ class PlayerSerializerTest {
@JsName("test4")
fun `test update player serialization`() {
test<PlayerUpdate>(updateJson) {
identifier shouldBe "..."
track.requirePresent {
identifier shouldBe "..."
}
endTime shouldBe 0
volume shouldBe 100
position shouldBe 32400
Expand Down

0 comments on commit 3c442f3

Please sign in to comment.