@@ -19,6 +19,7 @@ import net.ccbluex.liquidbounce.features.module.Category
1919import net.ccbluex.liquidbounce.features.module.Module
2020import net.ccbluex.liquidbounce.file.FileManager
2121import net.ccbluex.liquidbounce.ui.client.gui.GuiSpotify
22+ import net.ccbluex.liquidbounce.ui.client.gui.GuiSpotifyPlayer
2223import net.ccbluex.liquidbounce.ui.client.spotify.SpotifyAccessToken
2324import net.ccbluex.liquidbounce.ui.client.spotify.SpotifyConnectionChangedEvent
2425import net.ccbluex.liquidbounce.ui.client.spotify.SpotifyConnectionState
@@ -48,7 +49,7 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
4849 private var browserAuthFuture: CompletableFuture <SpotifyAccessToken >? = null
4950 private val credentialsFile = File (FileManager .dir, " spotify.json" )
5051 private val quickClientId: String = SpotifyDefaults .quickConnectClientId.trim()
51- private val supportedAuthModes = SpotifyAuthMode .values()
52+ private val supportedAuthModes = SpotifyAuthMode .entries
5253 .filter { it != SpotifyAuthMode .QUICK || quickClientId.isNotBlank() }
5354 .toTypedArray()
5455 private val defaultAuthMode = supportedAuthModes.firstOrNull() ? : SpotifyAuthMode .MANUAL
@@ -63,6 +64,14 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
6364 private val quickRefreshTokenValue = text(" QuickRefreshToken" , " " ).apply { hide() }
6465 private val pollIntervalValue = int(" PollInterval" , SpotifyDefaults .pollIntervalSeconds, 3 .. 60 , suffix = " s" )
6566 private val autoReconnectValue = boolean(" AutoReconnect" , true )
67+ private val openPlayerValue = boolean(" OpenUI" , false ).apply {
68+ onChange { _, newValue ->
69+ if (newValue) {
70+ mc.addScheduledTask { openPlayerScreen() }
71+ }
72+ false
73+ }
74+ }
6675 private val cachedTokens = EnumMap <SpotifyAuthMode , SpotifyAccessToken ?>(SpotifyAuthMode ::class .java)
6776
6877 init {
@@ -128,6 +137,11 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
128137 mc.displayGuiScreen(GuiSpotify (mc.currentScreen))
129138 }
130139
140+ fun openPlayerScreen () {
141+ reloadCredentialsFromDisk()
142+ mc.displayGuiScreen(GuiSpotifyPlayer (mc.currentScreen))
143+ }
144+
131145 fun updateCredentials (clientId : String , clientSecret : String , refreshToken : String ): Boolean {
132146 val sanitized = SpotifyCredentials (
133147 clientId.trim(),
@@ -145,9 +159,9 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
145159 return false
146160 }
147161
148- sanitized.clientId?.let { clientIdValue.changeValue (it) }
149- sanitized.clientSecret?.let { clientSecretValue.changeValue (it) }
150- sanitized.refreshToken?.let { refreshTokenValue.changeValue (it) }
162+ sanitized.clientId?.let { clientIdValue.set (it) }
163+ sanitized.clientSecret?.let { clientSecretValue.set (it) }
164+ sanitized.refreshToken?.let { refreshTokenValue.set (it) }
151165 val saved = persistCredentials()
152166 cachedTokens[SpotifyAuthMode .MANUAL ] = null
153167 if (state) {
@@ -189,6 +203,12 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
189203 return next
190204 }
191205
206+ suspend fun acquireAccessToken (forceRefresh : Boolean = false): SpotifyAccessToken ? {
207+ val mode = authMode
208+ val credentials = resolveCredentials(mode) ? : return null
209+ return ensureAccessToken(credentials, mode, forceRefresh)
210+ }
211+
192212 fun authModeLabel (): String = " Mode: ${authMode.displayName} "
193213
194214 fun supportsQuickConnect (): Boolean = supportedAuthModes.any { it == SpotifyAuthMode .QUICK }
@@ -237,8 +257,8 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
237257 }
238258
239259 when (mode) {
240- SpotifyAuthMode .QUICK -> quickRefreshTokenValue.changeValue (token.refreshToken)
241- SpotifyAuthMode .MANUAL -> refreshTokenValue.changeValue (token.refreshToken)
260+ SpotifyAuthMode .QUICK -> quickRefreshTokenValue.set (token.refreshToken)
261+ SpotifyAuthMode .MANUAL -> refreshTokenValue.set (token.refreshToken)
242262 }
243263 cachedTokens[mode] = token
244264 val saved = persistCredentials()
@@ -264,16 +284,12 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
264284
265285 private fun hasCredentials (): Boolean = resolveCredentials() != null
266286
267- private fun resolveCredentials (
268- mode : SpotifyAuthMode = authMode,
269- reasonConsumer : ((String ) -> Unit )? = null,
270- ): SpotifyCredentials ? {
287+ private fun resolveCredentials (mode : SpotifyAuthMode = authMode): SpotifyCredentials ? {
271288 val resolvedClientId = when (mode) {
272289 SpotifyAuthMode .QUICK -> quickClientId
273290 SpotifyAuthMode .MANUAL -> clientIdValue.get().trim()
274291 }
275292 if (resolvedClientId.isBlank()) {
276- reasonConsumer?.invoke(" Spotify client ID is not configured for ${mode.displayName} mode." )
277293 return null
278294 }
279295
@@ -282,18 +298,13 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
282298 SpotifyAuthMode .MANUAL -> refreshTokenValue.get().trim()
283299 }
284300 if (resolvedRefreshToken.isBlank()) {
285- reasonConsumer?.invoke(" Spotify refresh token is not configured for ${mode.displayName} mode." )
286301 return null
287302 }
288303
289304 val resolvedSecret = when (mode) {
290305 SpotifyAuthMode .QUICK -> " "
291306 SpotifyAuthMode .MANUAL -> clientSecretValue.get().trim()
292307 }
293- if (mode.flow == SpotifyAuthFlow .CONFIDENTIAL_CLIENT && resolvedSecret.isBlank()) {
294- reasonConsumer?.invoke(" Spotify client secret is not configured for ${mode.displayName} mode." )
295- return null
296- }
297308
298309 val credentials = SpotifyCredentials (
299310 resolvedClientId,
@@ -312,10 +323,9 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
312323 workerJob = moduleScope.launch {
313324 while (this @SpotifyModule.state) {
314325 val mode = authMode
315- val credentials = resolveCredentials(mode) { reason ->
316- handleError(reason)
317- }
326+ val credentials = resolveCredentials(mode)
318327 if (credentials == null ) {
328+ handleError(" Missing Spotify credentials (${mode.displayName} )" )
319329 delay(RETRY_DELAY_MS )
320330 continue
321331 }
@@ -339,12 +349,28 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
339349 }
340350 }
341351
352+ fun requestPlaybackRefresh () {
353+ moduleScope.launch {
354+ val mode = authMode
355+ val credentials = resolveCredentials(mode) ? : return @launch
356+ val token = ensureAccessToken(credentials, mode) ? : return @launch
357+ runCatching { service.fetchCurrentlyPlaying(token.value) }
358+ .onSuccess { state ->
359+ currentState = state
360+ EventManager .call(SpotifyStateChangedEvent (state))
361+ updateConnection(SpotifyConnectionState .CONNECTED , null )
362+ }
363+ .onFailure { handleError(" Failed to fetch playback: ${it.message} " ) }
364+ }
365+ }
366+
342367 private suspend fun ensureAccessToken (
343368 credentials : SpotifyCredentials ,
344369 mode : SpotifyAuthMode ,
370+ forceRefresh : Boolean = false,
345371 ): SpotifyAccessToken ? {
346372 val cached = cachedTokens[mode]
347- if (cached != null && cached.expiresAtMillis > System .currentTimeMillis() + TOKEN_EXPIRY_GRACE_MS ) {
373+ if (! forceRefresh && cached != null && cached.expiresAtMillis > System .currentTimeMillis() + TOKEN_EXPIRY_GRACE_MS ) {
348374 return cached
349375 }
350376
@@ -416,10 +442,10 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
416442 authModeValue.set(defaultAuthMode.storageValue)
417443 }
418444 }
419- obj.get(CONFIG_KEY_CLIENT_ID )?.takeIf { it.isJsonPrimitive }?.asString?.let { clientIdValue.changeValue (it) }
420- obj.get(CONFIG_KEY_CLIENT_SECRET )?.takeIf { it.isJsonPrimitive }?.asString?.let { clientSecretValue.changeValue (it) }
421- obj.get(CONFIG_KEY_REFRESH_TOKEN )?.takeIf { it.isJsonPrimitive }?.asString?.let { refreshTokenValue.changeValue (it) }
422- obj.get(CONFIG_KEY_QUICK_REFRESH_TOKEN )?.takeIf { it.isJsonPrimitive }?.asString?.let { quickRefreshTokenValue.changeValue (it) }
445+ obj.get(CONFIG_KEY_CLIENT_ID )?.takeIf { it.isJsonPrimitive }?.asString?.let { clientIdValue.set (it) }
446+ obj.get(CONFIG_KEY_CLIENT_SECRET )?.takeIf { it.isJsonPrimitive }?.asString?.let { clientSecretValue.set (it) }
447+ obj.get(CONFIG_KEY_REFRESH_TOKEN )?.takeIf { it.isJsonPrimitive }?.asString?.let { refreshTokenValue.set (it) }
448+ obj.get(CONFIG_KEY_QUICK_REFRESH_TOKEN )?.takeIf { it.isJsonPrimitive }?.asString?.let { quickRefreshTokenValue.set (it) }
423449
424450 cachedTokens[SpotifyAuthMode .MANUAL ] = restoreCachedToken(
425451 obj.get(CONFIG_KEY_ACCESS_TOKEN )?.takeIf { it.isJsonPrimitive }?.asString,
@@ -529,7 +555,7 @@ object SpotifyModule : Module("Spotify", Category.CLIENT, defaultState = false)
529555 MANUAL (" Manual" , " Custom App" , SpotifyAuthFlow .CONFIDENTIAL_CLIENT );
530556
531557 companion object {
532- fun fromStorage (value : String? ): SpotifyAuthMode ? = values() .firstOrNull {
558+ fun fromStorage (value : String? ): SpotifyAuthMode ? = SpotifyAuthMode .entries .firstOrNull {
533559 it.storageValue.equals(value, true )
534560 }
535561 }
0 commit comments