add network-dependent stream quality

Combine max bitrate and transcode format into one StreamQuality and store a separate value per network mode (Wi-Fi vs mobile data). NetworkMonitor reports metered vs unmetered via ConnectivityManager, SourceRepository selects and exposes the active quality, and PlaybackController reloads the window at the current position when it changes, so a network switch re-issues stream URLs with the new settings. Reset Coil's singleton in SubtracksApp.onCreate alongside Koin, since ArtworkSeedCache's background scope can call get() across Robolectric tests and poison the next Application.
This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent 8ba0aa5192
commit 117e8a69fb
13 changed files with 249 additions and 97 deletions
@@ -4,9 +4,9 @@ import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.subtracks.data.net.NetworkMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -34,6 +34,11 @@ data class ListQuery(
val starred: StarredFilter = StarredFilter.Any,
)
data class StreamQuality(
val maxBitrate: Int = 0,
val format: String? = null,
)
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
class UserPreferences(
@@ -48,24 +53,32 @@ class UserPreferences(
store.edit { prefs -> prefs[listQueryKey(tab)] = encode(query) }
}
val maxBitrate: Flow<Int> = store.data.map { prefs -> prefs[MAX_BITRATE] ?: 0 }
fun streamQuality(mode: NetworkMode): Flow<StreamQuality> =
store.data.map { prefs -> decodeStreamQuality(prefs[streamQualityKey(mode)]) }
suspend fun setMaxBitrate(kbps: Int) {
store.edit { prefs -> prefs[MAX_BITRATE] = kbps }
suspend fun setStreamQuality(
mode: NetworkMode,
quality: StreamQuality,
) {
store.edit { prefs -> prefs[streamQualityKey(mode)] = encode(quality) }
}
val streamFormat: Flow<String?> = store.data.map { prefs -> prefs[STREAM_FORMAT] }
suspend fun setStreamFormat(format: String?) {
store.edit { prefs ->
if (format.isNullOrEmpty()) prefs.remove(STREAM_FORMAT) else prefs[STREAM_FORMAT] = format
}
}
private fun streamQualityKey(mode: NetworkMode) = stringPreferencesKey("stream_quality_${mode.key}")
private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}")
private fun encode(query: ListQuery) = "${query.sort}|${if (query.descending) 1 else 0}|${query.starred.ordinal}"
private fun encode(quality: StreamQuality) = "${quality.maxBitrate}|${quality.format.orEmpty()}"
private fun decodeStreamQuality(stored: String?): StreamQuality {
val parts = stored?.split("|") ?: return StreamQuality()
return StreamQuality(
maxBitrate = parts.getOrNull(0)?.toIntOrNull() ?: 0,
format = parts.getOrNull(1)?.takeIf { it.isNotEmpty() },
)
}
private fun decode(
stored: String?,
defaultSort: String,
@@ -78,11 +91,6 @@ class UserPreferences(
starred = StarredFilter.entries.getOrElse(parts[2].toIntOrNull() ?: 0) { StarredFilter.Any },
)
}
private companion object {
val MAX_BITRATE = intPreferencesKey("max_bitrate")
val STREAM_FORMAT = stringPreferencesKey("stream_format")
}
}
fun createUserPreferences(context: Context): UserPreferences = UserPreferences(context.preferences)