Skip to content

Commit 3c442f3

Browse files
topi314duncte123
andauthored
Allow setting user data on tracks (#983)
* 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>
1 parent c2b6b09 commit 3c442f3

File tree

6 files changed

+129
-58
lines changed

6 files changed

+129
-58
lines changed

LavalinkServer/src/main/java/lavalink/server/player/PlayerRestHandler.kt

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,28 @@ class PlayerRestHandler(
5959
): ResponseEntity<Player> {
6060
val context = socketContext(socketServer, sessionId)
6161

62-
val encodedTrack = playerUpdate.encodedTrack
63-
if (encodedTrack is Omissible.Present && playerUpdate.identifier is Omissible.Present) {
62+
if (playerUpdate.track.isPresent() && (playerUpdate.encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present)) {
63+
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot specify both track and encodedTrack/identifier")
64+
}
65+
66+
val track = if (playerUpdate.track.isPresent()) {
67+
playerUpdate.track
68+
} else {
69+
if (playerUpdate.encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present) {
70+
PlayerUpdateTrack(
71+
playerUpdate.encodedTrack,
72+
playerUpdate.identifier
73+
).toOmissible()
74+
} else {
75+
Omissible.Omitted()
76+
}
77+
}
78+
79+
val encodedTrack = track.ifPresent { it.encoded } ?: Omissible.Omitted()
80+
val identifier = track.ifPresent { it.identifier } ?: Omissible.Omitted()
81+
val userData = track.ifPresent { it.userData } ?: Omissible.Omitted()
82+
83+
if (encodedTrack is Omissible.Present && identifier is Omissible.Present) {
6484
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot specify both encodedTrack and identifier")
6585
}
6686

@@ -112,25 +132,31 @@ class PlayerRestHandler(
112132

113133
// we handle pause differently for playing new tracks
114134
val paused = playerUpdate.paused
115-
paused.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
135+
paused.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
116136
?.let {
117137
player.setPause(it)
118138
}
119139

140+
// we handle userData differently for playing new tracks
141+
userData.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
142+
?.let {
143+
player.track?.userData = it
144+
}
145+
120146
playerUpdate.volume.ifPresent {
121147
player.setVolume(it)
122148
}
123149

124150
// we handle position differently for playing new tracks
125-
playerUpdate.position.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
151+
playerUpdate.position.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
126152
?.let {
127153
if (player.isPlaying) {
128154
player.seekTo(it)
129155
SocketServer.sendPlayerUpdate(context, player)
130156
}
131157
}
132158

133-
playerUpdate.endTime.takeIfPresent { encodedTrack is Omissible.Omitted && playerUpdate.identifier is Omissible.Omitted }
159+
playerUpdate.endTime.takeIfPresent { encodedTrack is Omissible.Omitted && identifier is Omissible.Omitted }
134160
?.let { endTime ->
135161
val marker = TrackMarker(endTime, TrackEndMarkerHandler(player))
136162
player.track?.setMarker(marker)
@@ -141,72 +167,75 @@ class PlayerRestHandler(
141167
SocketServer.sendPlayerUpdate(context, player)
142168
}
143169

144-
if (encodedTrack is Omissible.Present || playerUpdate.identifier is Omissible.Present) {
170+
if (encodedTrack is Omissible.Present || identifier is Omissible.Present) {
145171

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

152-
val track: AudioTrack? = if (encodedTrack is Omissible.Present) {
178+
val newTrack: AudioTrack? = if (encodedTrack is Omissible.Present) {
153179
encodedTrack.value?.let {
154180
decodeTrack(context.audioPlayerManager, it)
155181
}
156182
} else {
157183
val trackFuture = CompletableFuture<AudioTrack>()
158-
val identifier = playerUpdate.identifier as Omissible.Present
159-
context.audioPlayerManager.loadItem(identifier.value, object : AudioLoadResultHandler {
160-
override fun trackLoaded(track: AudioTrack) {
161-
trackFuture.complete(track)
162-
}
163-
164-
override fun playlistLoaded(playlist: AudioPlaylist) {
165-
trackFuture.completeExceptionally(
166-
ResponseStatusException(
167-
HttpStatus.BAD_REQUEST,
168-
"Cannot play a playlist or search result"
184+
context.audioPlayerManager.loadItemSync(
185+
(identifier as Omissible.Present).value,
186+
object : AudioLoadResultHandler {
187+
override fun trackLoaded(track: AudioTrack) {
188+
trackFuture.complete(track)
189+
}
190+
191+
override fun playlistLoaded(playlist: AudioPlaylist) {
192+
trackFuture.completeExceptionally(
193+
ResponseStatusException(
194+
HttpStatus.BAD_REQUEST,
195+
"Cannot play a playlist or search result"
196+
)
169197
)
170-
)
171-
}
172-
173-
override fun noMatches() {
174-
trackFuture.completeExceptionally(
175-
ResponseStatusException(
176-
HttpStatus.BAD_REQUEST,
177-
"No matches found for identifier"
198+
}
199+
200+
override fun noMatches() {
201+
trackFuture.completeExceptionally(
202+
ResponseStatusException(
203+
HttpStatus.BAD_REQUEST,
204+
"No matches found for identifier"
205+
)
178206
)
179-
)
180-
}
181-
182-
override fun loadFailed(exception: FriendlyException) {
183-
trackFuture.completeExceptionally(
184-
ResponseStatusException(
185-
HttpStatus.INTERNAL_SERVER_ERROR,
186-
exception.message,
187-
getRootCause(exception)
207+
}
208+
209+
override fun loadFailed(exception: FriendlyException) {
210+
trackFuture.completeExceptionally(
211+
ResponseStatusException(
212+
HttpStatus.INTERNAL_SERVER_ERROR,
213+
exception.message,
214+
getRootCause(exception)
215+
)
188216
)
189-
)
190-
}
191-
})
217+
}
218+
})
192219

193-
trackFuture.exceptionally {
194-
throw it
195-
}.join()
220+
trackFuture.join()
196221
}
197222

198-
track?.let {
223+
newTrack?.let {
199224
playerUpdate.position.ifPresent { position ->
200-
track.position = position
225+
newTrack.position = position
226+
}
227+
228+
userData.ifPresent { userData ->
229+
newTrack.userData = userData
201230
}
202231

203232
playerUpdate.endTime.ifPresent { endTime ->
204233
if (endTime != null) {
205-
track.setMarker(TrackMarker(endTime, TrackEndMarkerHandler(player)))
234+
newTrack.setMarker(TrackMarker(endTime, TrackEndMarkerHandler(player)))
206235
}
207236
}
208237

209-
player.play(track)
238+
player.play(newTrack)
210239
player.provideTo(context.getMediaConnection(player))
211240
} ?: player.stop()
212241
}

LavalinkServer/src/main/java/lavalink/server/util/util.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fun AudioTrack.toTrack(encoded: String, pluginInfoModifiers: List<AudioPluginInf
5454
acc + jsonObject
5555
}
5656

57-
return Track(encoded, this.toInfo(), pluginInfo)
57+
return Track(encoded, this.toInfo(), pluginInfo, this.userData as? JsonObject ?: JsonObject(emptyMap()))
5858
}
5959

6060
private operator fun JsonObject.plus(other: JsonObject) = JsonObject(toMap() + other.toMap())

protocol/src/commonMain/kotlin/dev/arbjerg/lavalink/protocol/v4/player.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ data class Player(
3131
data class Track(
3232
val encoded: String,
3333
val info: TrackInfo,
34-
val pluginInfo: JsonObject
34+
val pluginInfo: JsonObject,
35+
val userData: JsonObject
3536
) : LoadResult.Data {
3637

3738
/**
@@ -44,6 +45,28 @@ data class Track(
4445
* @return the deserialized plugin info as type T
4546
*/
4647
fun <T> deserializePluginInfo(deserializer: DeserializationStrategy<T>): T = pluginInfo.deserialize(deserializer)
48+
49+
/**
50+
* Deserialize the user data into a specific type.
51+
* This method is a convenience method meant to be used in Java,
52+
* since Kotlin extension methods are painful to use in Java.
53+
*
54+
* @param deserializer The deserializer to use. (e.g. `T.Companion.serializer()`)
55+
*
56+
* @return the deserialized user data as type T
57+
*/
58+
fun <T> deserializeUserData(deserializer: DeserializationStrategy<T>): T = userData.deserialize(deserializer)
59+
60+
/**
61+
* Copy this track with a new user data json.
62+
*
63+
* @param userData The new user data json.
64+
*
65+
* @return A copy of this track with the new user data json.
66+
*/
67+
fun copyWithUserData(userData: JsonObject): Track {
68+
return copy(userData = userData)
69+
}
4770
}
4871

4972
@Serializable
@@ -84,10 +107,20 @@ data class PlayerState(
84107
val ping: Long
85108
)
86109

110+
@Serializable
111+
data class PlayerUpdateTrack(
112+
val encoded: Omissible<String?> = Omissible.Omitted(),
113+
val identifier: Omissible<String> = Omissible.Omitted(),
114+
val userData: Omissible<JsonObject> = Omissible.Omitted()
115+
)
116+
87117
@Serializable
88118
data class PlayerUpdate(
119+
@Deprecated("Use PlayerUpdateTrack#encoded instead", ReplaceWith("encoded"))
89120
val encodedTrack: Omissible<String?> = Omissible.Omitted(),
121+
@Deprecated("Use PlayerUpdateTrack#identifier instead")
90122
val identifier: Omissible<String> = Omissible.Omitted(),
123+
val track: Omissible<PlayerUpdateTrack> = Omissible.Omitted(),
91124
val position: Omissible<Long> = Omissible.Omitted(),
92125
val endTime: Omissible<Long?> = Omissible.Omitted(),
93126
val volume: Omissible<Int> = Omissible.Omitted(),

protocol/src/commonTest/kotlin/LoadResultSerializerTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class LoadResultSerializerTest {
2929
"isrc": null,
3030
"sourceName": "youtube"
3131
},
32-
"pluginInfo": {}
32+
"pluginInfo": {},
33+
"userData": {}
3334
},
3435
"exception": null
3536
}

protocol/src/commonTest/kotlin/MessageSerializerTest.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ class MessageSerializerTest {
130130
"isrc": null,
131131
"sourceName": "youtube"
132132
},
133-
"pluginInfo": {}
133+
"pluginInfo": {},
134+
"userData": {}
134135
}
135136
}
136137
""".trimIndent()
@@ -183,7 +184,8 @@ class MessageSerializerTest {
183184
"isrc": null,
184185
"sourceName": "youtube"
185186
},
186-
"pluginInfo": {}
187+
"pluginInfo": {},
188+
"userData": {}
187189
},
188190
"reason": "finished"
189191
}
@@ -238,7 +240,8 @@ class MessageSerializerTest {
238240
"isrc": null,
239241
"sourceName": "youtube"
240242
},
241-
"pluginInfo": {}
243+
"pluginInfo": {},
244+
"userData": {}
242245
},
243246
"exception": {
244247
"message": "...",
@@ -301,7 +304,8 @@ class MessageSerializerTest {
301304
"isrc": null,
302305
"sourceName": "youtube"
303306
},
304-
"pluginInfo": {}
307+
"pluginInfo": {},
308+
"userData": {}
305309
},
306310
"thresholdMs": 123456789
307311
}

protocol/src/commonTest/kotlin/PlayerSerializerTest.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ private const val json = """
2727
"artworkUrl": null,
2828
"isrc": null
2929
},
30-
"pluginInfo": {}
30+
"pluginInfo": {},
31+
"userData": {}
3132
},
3233
"volume": 100,
3334
"paused": false,
@@ -52,7 +53,9 @@ private const val json = """
5253
//language=json
5354
const val updateJson = """
5455
{
55-
"identifier": "...",
56+
"track": {
57+
"identifier": "..."
58+
},
5659
"endTime": 0,
5760
"volume": 100,
5861
"position": 32400,
@@ -112,8 +115,7 @@ class PlayerSerializerTest {
112115
val json = """{}"""
113116

114117
test<PlayerUpdate>(json) {
115-
assertIs<Omissible.Omitted<*>>(encodedTrack)
116-
assertIs<Omissible.Omitted<*>>(identifier)
118+
assertIs<Omissible.Omitted<*>>(track)
117119
assertIs<Omissible.Omitted<*>>(position)
118120
assertIs<Omissible.Omitted<*>>(endTime)
119121
assertIs<Omissible.Omitted<*>>(volume)
@@ -127,7 +129,7 @@ class PlayerSerializerTest {
127129
@JsName("test3")
128130
fun `test encodedTrack and identifier exclusivity`() {
129131
//language=json
130-
val json = """{"encodedTrack": "", "identifier": ""}"""
132+
val json = """{"track": {"encoded": "", "identifier": ""}}"""
131133

132134
assertFailsWith<IllegalArgumentException> { Json.decodeFromString(json) }
133135
}
@@ -136,7 +138,9 @@ class PlayerSerializerTest {
136138
@JsName("test4")
137139
fun `test update player serialization`() {
138140
test<PlayerUpdate>(updateJson) {
139-
identifier shouldBe "..."
141+
track.requirePresent {
142+
identifier shouldBe "..."
143+
}
140144
endTime shouldBe 0
141145
volume shouldBe 100
142146
position shouldBe 32400

0 commit comments

Comments
 (0)