- Dependency Updates & Cleanup

- Frontend Security Enhancements (Nginx/Entrypoint)
- Docker Networking & Binding
- Code Bug Fix
This commit is contained in:
2026-05-19 10:11:15 +02:00
parent 6146bf18b0
commit 8247692aa2
2285 changed files with 1801 additions and 485963 deletions
+6 -3
View File
@@ -5,7 +5,8 @@ plugins {
android {
namespace = "com.groove.audio"
compileSdk = 35
compileSdk = 36
buildToolsVersion = "35.0.0"
defaultConfig {
applicationId = "com.groove.audio"
@@ -38,13 +39,14 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
kotlinCompilerExtensionVersion = "1.5.14"
}
}
dependencies {
// Media3 (ExoPlayer successor — recommended for new projects)
implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("androidx.media3:media3-datasource-okhttp:1.4.1")
implementation("androidx.media3:media3-ui:1.4.1")
implementation("androidx.media3:media3-session:1.4.1") // MediaSession + notification
@@ -60,7 +62,8 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.ui:ui:1.6.8")
implementation("androidx.compose.material:material:1.6.8")
implementation("androidx.compose.material3:material3:1.2.1")
implementation("androidx.compose.material:material-icons-extended:1.6.8")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.8")
implementation("androidx.navigation:navigation-compose:2.7.7")
}
@@ -5,14 +5,15 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.GrooveAudio"
tools:targetApi="31">
@@ -20,7 +21,7 @@
<service
android:name=".audio.GroovePlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
@@ -1,10 +1,8 @@
package com.groove.audio.mopidy
import androidx.media3.common.util.UnstableApi
import kotlinx.serialization.Serializable
@UnstableApi
@Serializable
data class Track(
val name: String? = null,
val artists: List<Artist>? = null,
@@ -14,24 +12,20 @@ data class Track(
val metadata: Map<String, String>? = null
)
@Serializable
data class Artist(
val name: String? = null
)
@Serializable
data class Album(
val name: String? = null,
val artists: List<Artist>? = null
)
@Serializable
data class PlaybackState(
val state: String? = null,
val tlTrack: TLTrack? = null
)
@Serializable
data class TLTrack(
val track: Track? = null
)
@@ -5,25 +5,32 @@ import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.util.concurrent.atomic.AtomicInteger
@UnstableApi
class MopidyClient(private val baseUrl: String) {
class MopidyClient(host: String) {
private val baseUrl = "http://${host.trim().removePrefix("http://").removePrefix("https://")}:8180"
private val http = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
private val jsonMediaType = "application/json".toMediaType()
private var idCounter = 1
private val idCounter = AtomicInteger(1)
suspend fun call(method: String, params: JsonObject = buildJsonObject {}): JsonObject {
suspend fun call(method: String, params: JsonObject = buildJsonObject {}): JsonElement {
val body = buildJsonObject {
put("jsonrpc", "2.0")
put("id", idCounter++)
put("id", idCounter.getAndIncrement())
put("method", method)
put("params", params)
}.toString().toRequestBody(jsonMediaType)
@@ -35,15 +42,44 @@ class MopidyClient(private val baseUrl: String) {
return withContext(Dispatchers.IO) {
http.newCall(request).execute().use { response ->
val text = response.body?.string() ?: "{}"
try {
val obj = json.parseToJsonElement(text).jsonObject
obj["result"]?.jsonObject ?: JsonObject(emptyMap())
} catch (e: Exception) {
Log.e("MopidyClient", "JSON parse error: ${e.message}", e)
JsonObject(emptyMap())
val text = response.body?.string().orEmpty()
if (!response.isSuccessful) {
throw IOException("Mopidy HTTP ${response.code}: $text")
}
val obj = json.parseToJsonElement(text).jsonObject
obj["error"]?.let { throw IOException("Mopidy RPC error: $it") }
obj["result"] ?: JsonNull
}
}
}
suspend fun getState(): String =
call("core.playback.get_state").jsonPrimitive.content
suspend fun getCurrentTlTrack(): JsonObject? =
(call("core.playback.get_current_tl_track") as? JsonObject)
suspend fun getVolume(): Int =
call("core.mixer.get_volume").jsonPrimitive.content.toIntOrNull() ?: 70
suspend fun setVolume(volume: Int) {
call("core.mixer.set_volume", buildJsonObject {
put("volume", volume.coerceIn(0, 100))
})
}
suspend fun play() {
call("core.playback.play")
}
suspend fun pause() {
call("core.playback.pause")
}
companion object {
fun logFailure(tag: String, message: String, error: Throwable) {
Log.e(tag, "$message: ${error.message}", error)
}
}
}
@@ -4,6 +4,7 @@ import android.util.Log
import androidx.media3.common.util.UnstableApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
@@ -1,6 +1,6 @@
package com.groove.audio.ui
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
@@ -25,6 +27,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -34,9 +37,10 @@ import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import coil.compose.rememberAsyncImagePainter
import com.groove.audio.AppSettings
import com.groove.audio.audio.GroovePlaybackService
import com.groove.audio.mopidy.MopidyClient
import com.groove.audio.mopidy.PlaybackState
import kotlinx.serialization.json.JsonObject
import kotlinx.coroutines.launch
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -49,31 +53,41 @@ fun NowPlayingScreen() {
var isPlaying by remember { mutableStateOf(false) }
var volume by remember { mutableStateOf(70) }
var coverArtUrl by remember { mutableStateOf<String?>(null) }
var showSettings by remember { mutableStateOf(false) }
val mopidyClient = remember { MopidyClient(settings.host) }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
context.startForegroundService(Intent(context, GroovePlaybackService::class.java))
}
if (showSettings) {
SettingsScreen(onBack = { showSettings = false })
return
}
LaunchedEffect(Unit) {
// Poll for playback state
while (true) {
try {
val result = mopidyClient.call("core.playback.getState")
val state = result.jsonObject["state"]?.jsonPrimitive?.content
val track = result.jsonObject["tl_track"]?.jsonObject
val trackName = track?.jsonObject?.get("track")?.jsonObject?.get("name")?.jsonPrimitive?.content
val artists = track?.jsonObject?.get("track")?.jsonObject?.get("artists")?.jsonArray
val state = mopidyClient.getState()
val track = mopidyClient.getCurrentTlTrack()
?.get("track")
?.jsonObject
val trackName = track?.get("name")?.jsonPrimitive?.content
val artists = track?.get("artists")?.jsonArray
val artistName = artists?.getOrNull(0)?.jsonObject?.get("name")?.jsonPrimitive?.content
val albumName = track?.jsonObject?.get("track")?.jsonObject?.get("album")?.jsonObject?.get("name")?.jsonPrimitive?.content
val coverArt = track?.jsonObject?.get("track")?.jsonObject?.get("metadata")?.jsonObject?.get("images")?.jsonArray
currentTrack = if (trackName != null && artistName != null) {
"$trackName - $artistName"
} else {
"Unknown Track"
trackName ?: "Unknown Track"
}
isPlaying = state == "playing"
} catch (e: Exception) {
println("Error fetching playback state: ${e.message}")
MopidyClient.logFailure("NowPlayingScreen", "Error fetching playback state", e)
}
// Poll every 3 seconds
@@ -84,10 +98,9 @@ fun NowPlayingScreen() {
LaunchedEffect(Unit) {
// Fetch volume
try {
val result = mopidyClient.call("core.mixer.getVolume")
volume = result.jsonObject["volume"]?.jsonPrimitive?.int ?: 70
volume = mopidyClient.getVolume()
} catch (e: Exception) {
println("Error fetching volume: ${e.message}")
MopidyClient.logFailure("NowPlayingScreen", "Error fetching volume", e)
}
}
@@ -98,6 +111,19 @@ fun NowPlayingScreen() {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
IconButton(onClick = { showSettings = true }) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onBackground
)
}
}
// Cover Art
val painter = rememberAsyncImagePainter(coverArtUrl)
Image(
@@ -122,14 +148,16 @@ fun NowPlayingScreen() {
// Play/Pause Button
Button(onClick = {
try {
if (isPlaying) {
mopidyClient.call("core.playback.pause")
} else {
mopidyClient.call("core.playback.play")
scope.launch {
try {
if (isPlaying) {
mopidyClient.pause()
} else {
mopidyClient.play()
}
} catch (e: Exception) {
MopidyClient.logFailure("NowPlayingScreen", "Error toggling play/pause", e)
}
} catch (e: Exception) {
println("Error toggling play/pause: ${e.message}")
}
}) {
Icon(
@@ -153,11 +181,14 @@ fun NowPlayingScreen() {
Slider(
value = volume.toFloat(),
onValueChange = { newValue ->
volume = newValue.toInt()
try {
mopidyClient.call("core.mixer.setVolume", JsonObject(mapOf("volume" to volume)))
} catch (e: Exception) {
println("Error setting volume: ${e.message}")
val newVolume = newValue.toInt()
volume = newVolume
scope.launch {
try {
mopidyClient.setVolume(newVolume)
} catch (e: Exception) {
MopidyClient.logFailure("NowPlayingScreen", "Error setting volume", e)
}
}
},
valueRange = 0f..100f
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#101418"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#3DDC97"
android:pathData="M38,22h10v42a14,14 0,1 1,-10 -13z" />
<path
android:fillColor="#61A8FF"
android:pathData="M60,22h10v50a14,14 0,1 1,-10 -13z" />
</vector>
@@ -0,0 +1,3 @@
<resources>
<color name="groove_background">#101418</color>
</resources>
@@ -0,0 +1,6 @@
<resources>
<style name="Theme.GrooveAudio" parent="android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/groove_background</item>
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include domain="sharedpref" path="." />
</full-backup-content>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include domain="sharedpref" path="." />
</cloud-backup>
<device-transfer>
<include domain="sharedpref" path="." />
</device-transfer>
</data-extraction-rules>