mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
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:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,18 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.prefs.fakeUserPreferences
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -20,6 +27,7 @@ import org.junit.runner.RunWith
|
||||
class SourceRepositoryTest {
|
||||
private lateinit var db: SubtracksDatabase
|
||||
private lateinit var repository: SourceRepository
|
||||
private val messages = ArrayList<String>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -29,7 +37,7 @@ class SourceRepositoryTest {
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
repository = SourceRepository(db, OkHttpClient(), fakeUserPreferences())
|
||||
repository = SourceRepository(db, OkHttpClient(), fakeUserPreferences(), showMessage = { messages += it })
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -57,4 +65,49 @@ class SourceRepositoryTest {
|
||||
.address,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aTokenAuthSourceIsProbedAndSwitchedToPassword() =
|
||||
runBlocking {
|
||||
withServer { server ->
|
||||
server.enqueue(MockResponse().setBody(failed(41)))
|
||||
server.enqueue(MockResponse().setBody(OK))
|
||||
|
||||
repository.addSource("nav", server.url("/").toString(), "u", "s3cret", true)
|
||||
|
||||
val config = withTimeout(5_000) { repository.activeConfig().first { it != null && !it.useTokenAuth } }
|
||||
assertFalse(config!!.useTokenAuth)
|
||||
withTimeout(5_000) { while (messages.isEmpty()) delay(10) }
|
||||
assertEquals(1, messages.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pingReportsWhetherItFellBackToThePassword() =
|
||||
runBlocking {
|
||||
withServer { server ->
|
||||
server.enqueue(MockResponse().setBody(failed(41)))
|
||||
server.enqueue(MockResponse().setBody(OK))
|
||||
server.enqueue(MockResponse().setBody(OK))
|
||||
|
||||
assertTrue(repository.ping(server.url("/").toString(), "u", "s3cret", true).getOrThrow())
|
||||
assertFalse(repository.ping(server.url("/").toString(), "u", "s3cret", false).getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun withServer(block: suspend (MockWebServer) -> Unit) {
|
||||
val server = MockWebServer()
|
||||
server.start()
|
||||
try {
|
||||
block(server)
|
||||
} finally {
|
||||
server.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun failed(code: Int) = "<subsonic-response status=\"failed\"><error code=\"$code\" message=\"nope\"/></subsonic-response>"
|
||||
|
||||
private companion object {
|
||||
const val OK = "<subsonic-response status=\"ok\" version=\"1.16.1\"/>"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class SubsonicClientTest {
|
||||
|
||||
private fun client(
|
||||
tokenAuth: Boolean,
|
||||
onTokenAuthUnsupported: (() -> Unit)? = null,
|
||||
onTokenAuthUnsupported: () -> Unit = {},
|
||||
) = SubsonicClient(
|
||||
baseUrl = server.url("/"),
|
||||
username = "guest",
|
||||
|
||||
Reference in New Issue
Block a user