release v2026.05.11.3: caching, perf, playlist polish #41
@@ -1,5 +1,6 @@
|
|||||||
package com.fabledsword.minstrel.auth
|
package com.fabledsword.minstrel.auth
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.cache.audiocache.CacheSettings
|
||||||
import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao
|
import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao
|
||||||
import com.fabledsword.minstrel.cache.db.entities.AuthSessionEntity
|
import com.fabledsword.minstrel.cache.db.entities.AuthSessionEntity
|
||||||
import com.fabledsword.minstrel.di.ApplicationScope
|
import com.fabledsword.minstrel.di.ApplicationScope
|
||||||
@@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -46,6 +48,11 @@ class AuthStore @Inject constructor(
|
|||||||
private val clientIdState = MutableStateFlow<String?>(null)
|
private val clientIdState = MutableStateFlow<String?>(null)
|
||||||
val clientId: StateFlow<String?> = clientIdState.asStateFlow()
|
val clientId: StateFlow<String?> = clientIdState.asStateFlow()
|
||||||
|
|
||||||
|
private val cacheSettingsState = MutableStateFlow(CacheSettings.DEFAULT)
|
||||||
|
val cacheSettings: StateFlow<CacheSettings> = cacheSettingsState.asStateFlow()
|
||||||
|
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dao.observe().collect { row ->
|
dao.observe().collect { row ->
|
||||||
@@ -54,10 +61,18 @@ class AuthStore @Inject constructor(
|
|||||||
userJsonState.value = row?.userJson
|
userJsonState.value = row?.userJson
|
||||||
themeModeState.value = row?.themeMode
|
themeModeState.value = row?.themeMode
|
||||||
clientIdState.value = row?.clientId
|
clientIdState.value = row?.clientId
|
||||||
|
cacheSettingsState.value = decodeCacheSettings(row?.cacheSettingsJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeCacheSettings(raw: String?): CacheSettings {
|
||||||
|
if (raw.isNullOrEmpty()) return CacheSettings.DEFAULT
|
||||||
|
return runCatching {
|
||||||
|
json.decodeFromString(CacheSettings.serializer(), raw)
|
||||||
|
}.getOrDefault(CacheSettings.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
fun setSessionCookie(value: String?) {
|
fun setSessionCookie(value: String?) {
|
||||||
sessionCookieState.value = value
|
sessionCookieState.value = value
|
||||||
scope.launch { persistCookie(value) }
|
scope.launch { persistCookie(value) }
|
||||||
@@ -83,6 +98,12 @@ class AuthStore @Inject constructor(
|
|||||||
scope.launch { persistClientId(value) }
|
scope.launch { persistClientId(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCacheSettings(value: CacheSettings) {
|
||||||
|
cacheSettingsState.value = value
|
||||||
|
val encoded = json.encodeToString(CacheSettings.serializer(), value)
|
||||||
|
scope.launch { persistCacheSettings(encoded) }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun persistCookie(value: String?) {
|
private suspend fun persistCookie(value: String?) {
|
||||||
if (dao.get() == null) {
|
if (dao.get() == null) {
|
||||||
dao.upsert(currentEntity().copy(sessionCookie = value))
|
dao.upsert(currentEntity().copy(sessionCookie = value))
|
||||||
@@ -123,6 +144,14 @@ class AuthStore @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun persistCacheSettings(json: String) {
|
||||||
|
if (dao.get() == null) {
|
||||||
|
dao.upsert(currentEntity().copy(cacheSettingsJson = json))
|
||||||
|
} else {
|
||||||
|
dao.setCacheSettingsJson(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun currentEntity(): AuthSessionEntity = AuthSessionEntity(
|
private fun currentEntity(): AuthSessionEntity = AuthSessionEntity(
|
||||||
id = ROW_ID,
|
id = ROW_ID,
|
||||||
sessionCookie = sessionCookieState.value,
|
sessionCookie = sessionCookieState.value,
|
||||||
@@ -130,6 +159,10 @@ class AuthStore @Inject constructor(
|
|||||||
userJson = userJsonState.value,
|
userJson = userJsonState.value,
|
||||||
themeMode = themeModeState.value,
|
themeMode = themeModeState.value,
|
||||||
clientId = clientIdState.value,
|
clientId = clientIdState.value,
|
||||||
|
cacheSettingsJson = json.encodeToString(
|
||||||
|
CacheSettings.serializer(),
|
||||||
|
cacheSettingsState.value,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.fabledsword.minstrel.cache.audiocache
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
private const val FIVE_GIB_BYTES = 5L * 1024 * 1024 * 1024
|
||||||
|
private const val DEFAULT_PREFETCH_WINDOW = 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-tunable audio cache settings. Mirrors Flutter's `CacheSettings`
|
||||||
|
* (cache_settings_provider.dart) field-for-field. Persisted as a JSON
|
||||||
|
* blob on the auth_session single-row table via [AuthStore].
|
||||||
|
*
|
||||||
|
* - [likedCapBytes]: budget for cached files of liked tracks. 0 means
|
||||||
|
* unlimited. Effective at next app start (SimpleCache is built once
|
||||||
|
* per process).
|
||||||
|
* - [rollingCapBytes]: budget for incidental / auto-prefetch downloads.
|
||||||
|
* Same 0=unlimited / restart-to-apply caveat.
|
||||||
|
* - [prefetchWindow]: 1..10. Number of next-tracks the prefetcher
|
||||||
|
* pre-downloads. Has no effect until the prefetcher lands (separate
|
||||||
|
* audit follow-up) — persisted now so the user can pre-configure.
|
||||||
|
* - [cacheLikedTracks]: when true, liking a track should also pin its
|
||||||
|
* audio to the liked-cache bucket. Has no effect until pin-on-like
|
||||||
|
* lands (separate follow-up).
|
||||||
|
*
|
||||||
|
* Defaults match Flutter: 5 GiB per bucket, prefetch=5, cache-liked=true.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class CacheSettings(
|
||||||
|
val likedCapBytes: Long = FIVE_GIB_BYTES,
|
||||||
|
val rollingCapBytes: Long = FIVE_GIB_BYTES,
|
||||||
|
val prefetchWindow: Int = DEFAULT_PREFETCH_WINDOW,
|
||||||
|
val cacheLikedTracks: Boolean = true,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT: CacheSettings = CacheSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
|
|||||||
CachedHomeIndexEntity::class,
|
CachedHomeIndexEntity::class,
|
||||||
AuthSessionEntity::class,
|
AuthSessionEntity::class,
|
||||||
],
|
],
|
||||||
version = 4,
|
version = 5,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(MinstrelTypeConverters::class)
|
@TypeConverters(MinstrelTypeConverters::class)
|
||||||
|
|||||||
+4
@@ -38,4 +38,8 @@ interface AuthSessionDao {
|
|||||||
/** Partial update: change only the stable client install id. */
|
/** Partial update: change only the stable client install id. */
|
||||||
@Query("UPDATE auth_session SET clientId = :clientId WHERE id = 0")
|
@Query("UPDATE auth_session SET clientId = :clientId WHERE id = 0")
|
||||||
suspend fun setClientId(clientId: String?)
|
suspend fun setClientId(clientId: String?)
|
||||||
|
|
||||||
|
/** Partial update: change only the serialized cache settings. */
|
||||||
|
@Query("UPDATE auth_session SET cacheSettingsJson = :json WHERE id = 0")
|
||||||
|
suspend fun setCacheSettingsJson(json: String?)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -29,4 +29,11 @@ data class AuthSessionEntity(
|
|||||||
val userJson: String? = null,
|
val userJson: String? = null,
|
||||||
val themeMode: String? = null,
|
val themeMode: String? = null,
|
||||||
val clientId: String? = null,
|
val clientId: String? = null,
|
||||||
|
/**
|
||||||
|
* JSON-encoded CacheSettings (cache/audiocache/CacheSettings.kt).
|
||||||
|
* Null = use defaults. Stored as a JSON blob rather than four
|
||||||
|
* individual columns to keep schema changes cheap when the
|
||||||
|
* CacheSettings shape evolves.
|
||||||
|
*/
|
||||||
|
val cacheSettingsJson: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.fabledsword.minstrel.player
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.auth.AuthStore
|
||||||
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -21,7 +22,23 @@ import javax.inject.Singleton
|
|||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object PlayerModule {
|
object PlayerModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CacheConfig snapshot from the user's persisted CacheSettings.
|
||||||
|
* SimpleCache is constructed once per process, so changes via the
|
||||||
|
* Settings UI take effect on next app launch (not mid-flight).
|
||||||
|
* AuthStore's StateFlow hydrates async from Room — when the
|
||||||
|
* factory @Provides runs, the value is either the persisted
|
||||||
|
* setting (if Room finished first) or CacheSettings.DEFAULT.
|
||||||
|
* Worst case the user's tuning lands on next-next launch, which
|
||||||
|
* is acceptable for a single-user device pref.
|
||||||
|
*/
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCacheConfig(): CacheConfig = CacheConfig()
|
fun provideCacheConfig(authStore: AuthStore): CacheConfig {
|
||||||
|
val s = authStore.cacheSettings.value
|
||||||
|
return CacheConfig(
|
||||||
|
likedCapBytes = s.likedCapBytes,
|
||||||
|
rollingCapBytes = s.rollingCapBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user