address review: probe the active source so media falls back too

An existing token-auth source with an already-synced library would fail playback before
any API call ran: stream and cover-art URLs are built by uri() and fetched by
ExoPlayer/Coil, outside stream(). Probe the active source once when it uses token auth so
the 41 flips and persists before any URL is built.

Also base the retry on the flag the request actually used (concurrent requests no longer
lose their retry), report the fallback from ping() so Add Source flips the toggle instead
of reporting success and saving token auth on, and make the callback non-null.
This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent 59706f28c1
commit 9c97f64381
5 changed files with 98 additions and 23 deletions
@@ -31,6 +31,7 @@ class SourceRepository(
@Volatile
private var active: SubsonicMusicSource? = null
private var probedTokenAuthId: Long? = null
init {
scope.launch {
@@ -38,8 +39,16 @@ class SourceRepository(
db.sourcesDao().activeSubsonicConfig(),
prefs.maxBitrate,
prefs.streamFormat,
) { config, maxBitrate, streamFormat -> config?.toMusicSource(maxBitrate, streamFormat) }
.collect { active = it }
) { config, maxBitrate, streamFormat -> Triple(config, maxBitrate, streamFormat) }
.collect { (config, maxBitrate, streamFormat) ->
active = config?.toMusicSource(maxBitrate, streamFormat)
config
?.takeIf { it.useTokenAuth && it.id != probedTokenAuthId }
?.let { source ->
probedTokenAuthId = source.id
scope.launch { runCatching { source.toClient().check("ping") } }
}
}
}
}
@@ -111,9 +120,12 @@ class SourceRepository(
username: String,
password: String,
useTokenAuth: Boolean,
): Result<Unit> =
): Result<Boolean> =
withContext(Dispatchers.IO) {
runCatching { client(address, username, password, useTokenAuth).check("ping") }
var fellBack = false
runCatching {
client(address, username, password, useTokenAuth) { fellBack = true }.check("ping")
}.map { fellBack }
}
suspend fun activeMusicSource(): MusicSource? {
@@ -121,20 +133,22 @@ class SourceRepository(
return config.toMusicSource(prefs.maxBitrate.first(), prefs.streamFormat.first())
}
private fun SubsonicConfig.toClient(): SubsonicClient =
client(
address = address,
username = username,
password = password,
useTokenAuth = useTokenAuth,
onTokenAuthUnsupported = { disableTokenAuth(id) },
)
private fun SubsonicConfig.toMusicSource(
maxBitrate: Int,
streamFormat: String?,
): SubsonicMusicSource =
SubsonicMusicSource(
id = id,
client =
client(
address = address,
username = username,
password = password,
useTokenAuth = useTokenAuth,
onTokenAuthUnsupported = { disableTokenAuth(id) },
),
client = toClient(),
maxBitrate = maxBitrate,
streamFormat = streamFormat,
)
@@ -151,7 +165,7 @@ class SourceRepository(
username: String,
password: String,
useTokenAuth: Boolean,
onTokenAuthUnsupported: (() -> Unit)? = null,
onTokenAuthUnsupported: () -> Unit = {},
) = SubsonicClient(
baseUrl = normalizeAddress(address).toHttpUrl(),
username = username,
@@ -18,7 +18,7 @@ class SubsonicClient(
useTokenAuth: Boolean,
private val http: OkHttpClient,
private val userAgent: String = "subtracks/android",
private val onTokenAuthUnsupported: (() -> Unit)? = null,
private val onTokenAuthUnsupported: () -> Unit = {},
) {
@Volatile
private var useTokenAuth: Boolean = useTokenAuth
@@ -53,14 +53,16 @@ class SubsonicClient(
method: String,
params: Map<String, String> = emptyMap(),
body: (InputStream) -> T,
): T =
try {
): T {
val usedTokenAuth = useTokenAuth
return try {
execute(method, params, body)
} catch (failure: SubsonicException) {
if (failure.code != TOKEN_AUTH_UNSUPPORTED || !useTokenAuth) throw failure
if (failure.code != TOKEN_AUTH_UNSUPPORTED || !usedTokenAuth) throw failure
disableTokenAuth()
execute(method, params, body)
}
}
private fun <T> execute(
method: String,
@@ -85,7 +87,7 @@ class SubsonicClient(
private fun disableTokenAuth() {
if (!useTokenAuth) return
useTokenAuth = false
onTokenAuthUnsupported?.invoke()
onTokenAuthUnsupported()
}
companion object {
@@ -42,10 +42,16 @@ class AddSourceViewModel(
val current = _state.value
viewModelScope.launch {
val result = sourceRepository.ping(current.address, current.username, current.password, current.useTokenAuth)
_state.update {
it.copy(
_state.update { state ->
val fellBack = result.getOrDefault(false)
state.copy(
busy = false,
message = result.fold({ "Connection OK" }, { error -> "Failed: ${error.message}" }),
useTokenAuth = if (fellBack) false else state.useTokenAuth,
message =
result.fold(
{ if (fellBack) "Server does not support token auth; using the password instead" else "Connection OK" },
{ error -> "Failed: ${error.message}" },
),
isError = result.isFailure,
)
}