Skip to content

Commit 95560f7

Browse files
committed
feat: Spotify UI option
1 parent d32b6b2 commit 95560f7

File tree

24 files changed

+2015
-306
lines changed

24 files changed

+2015
-306
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ minecraft {
4242
def spotifyHttpTimeout = (project.findProperty("spotify_http_timeout_ms") ?: "12000").toString()
4343
def spotifyDashboardUrl = (project.findProperty("spotify_dashboard_url") ?: "https://developer.spotify.com/dashboard").toString()
4444
def spotifyAuthGuideUrl = (project.findProperty("spotify_authorization_guide_url") ?: "https://developer.spotify.com/documentation/web-api/tutorials/refreshing-tokens").toString()
45-
def spotifyAuthScopes = (project.findProperty("spotify_authorization_scopes") ?: "user-read-currently-playing user-read-playback-state").toString()
45+
def spotifyAuthScopes = (project.findProperty("spotify_authorization_scopes") ?: "user-read-currently-playing user-read-playback-state user-modify-playback-state playlist-read-private playlist-read-collaborative user-library-read").toString()
4646
def spotifyAuthRedirectPort = (project.findProperty("spotify_authorization_redirect_port") ?: "43791").toString()
4747
def spotifyAuthRedirectPath = (project.findProperty("spotify_authorization_redirect_path") ?: "/spotify-oauth-callback").toString()
4848

src/main/java/net/ccbluex/liquidbounce/features/module/modules/client/SpotifyModule.kt

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import net.ccbluex.liquidbounce.features.module.Category
1919
import net.ccbluex.liquidbounce.features.module.Module
2020
import net.ccbluex.liquidbounce.file.FileManager
2121
import net.ccbluex.liquidbounce.ui.client.gui.GuiSpotify
22+
import net.ccbluex.liquidbounce.ui.client.gui.GuiSpotifyPlayer
2223
import net.ccbluex.liquidbounce.ui.client.spotify.SpotifyAccessToken
2324
import net.ccbluex.liquidbounce.ui.client.spotify.SpotifyConnectionChangedEvent
2425
import 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

Comments
 (0)