Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions app/src/main/java/app/waveflow/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,10 @@ private fun WaveFlowRoot(
serverState.connected?.let { session ->
val qualityState by qualityViewModel.state.collectAsStateWithLifecycle()

// La taille bouge à chaque piste lue, et ffmpeg peut
// avoir été installé ou retiré du serveur : les deux
// se relèvent à l'ouverture, pas une fois pour toutes.
// La taille bouge à chaque piste lue : elle se relève
// à l'ouverture, pas une fois pour toutes.
LaunchedEffect(Unit) {
cacheViewModel.refresh()
qualityViewModel.refresh()
}

ServerAccountScreen(
Expand Down
8 changes: 0 additions & 8 deletions app/src/main/java/app/waveflow/data/remote/CatalogApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,6 @@ interface CatalogApi {
trackId: String,
rendering: StreamRendering,
): String

/**
* `GET /api/v2/transcode/status` : le serveur sait-il transcoder ?
*
* Un serveur sans ffmpeg sert les originaux et rien d'autre. Lui demander
* une version transcodée ferait échouer chaque piste.
*/
suspend fun transcodingAvailable(serverUrl: String, accessToken: String): Boolean
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ class CatalogRepository(
suspend fun streamUrl(trackId: String, rendering: StreamRendering): String =
authorized { url, token -> api.streamTicket(url, token, trackId, rendering) }

/** Voir [CatalogApi.transcodingAvailable]. */
suspend fun transcodingAvailable(): Boolean =
authorized { url, token -> api.transcodingAvailable(url, token) }

/**
* Exécute [call] avec un jeton valide, en réessayant une fois sur refus.
*
Expand Down
12 changes: 0 additions & 12 deletions app/src/main/java/app/waveflow/data/remote/Dto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,6 @@ internal data class StreamTicketResponse(
@SerialName("expires_at") val expiresAt: Long,
)

/**
* `GET /api/v2/transcode/status`.
*
* Le serveur rend aussi `active`, `global_limit` et `per_user_limit` ; seul
* `available` sert aujourd'hui, et un champ lu sans être utilisé serait un
* champ de plus à croire à jour.
*/
@Serializable
internal data class TranscodeStatusResponse(
val available: Boolean,
)

@Serializable
internal data class ArtistDetailResponse(
val id: String,
Expand Down
8 changes: 0 additions & 8 deletions app/src/main/java/app/waveflow/data/remote/HttpCatalogApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,6 @@ class HttpCatalogApi(
return http.absoluteUrl(serverUrl, ticket.url, query = rendering.toQuery())
}

override suspend fun transcodingAvailable(serverUrl: String, accessToken: String): Boolean =
http.get(
serverUrl = serverUrl,
path = TRANSCODE_STATUS,
accessToken = accessToken,
).decode<TranscodeStatusResponse>().available

/** L'original ne dit rien : le serveur le sert par défaut, et refuse qu'on lui donne un débit. */
private fun StreamRendering.toQuery(): Map<String, String> =
if (isOriginal) {
Expand All @@ -141,7 +134,6 @@ class HttpCatalogApi(
}

private companion object {
const val TRANSCODE_STATUS = "api/v2/transcode/status"
const val ALBUMS = "api/v2/albums"
const val ARTISTS = "api/v2/artists"
const val TRACKS = "api/v2/tracks"
Expand Down
40 changes: 7 additions & 33 deletions app/src/main/java/app/waveflow/ui/quality/StreamQualitySection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,18 @@ fun StreamQualitySection(
QualityRow(
quality = quality,
selected = quality == state.current,
enabled = state.isSelectable(quality),
onChoose = onChoose,
)
}
}

Text(
text = note(state),
// Le rendu est posé sur chaque piste quand elle entre dans la
// file : changer d'avis en pleine écoute sans que rien ne bouge
// se lirait sinon comme une panne.
text = "S'applique aux pistes lancées ensuite, pas à la file en cours.",
style = MaterialTheme.typography.bodySmall,
color = if (state.isCurrentUnavailable) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
Expand All @@ -76,7 +74,6 @@ fun StreamQualitySection(
private fun QualityRow(
quality: StreamQuality,
selected: Boolean,
enabled: Boolean,
onChoose: (StreamQuality) -> Unit,
) {
Row(
Expand All @@ -85,25 +82,20 @@ private fun QualityRow(
.fillMaxWidth()
.selectable(
selected = selected,
enabled = enabled,
role = Role.RadioButton,
onClick = { onChoose(quality) },
)
.padding(horizontal = 16.dp, vertical = 10.dp),
) {
RadioButton(selected = selected, onClick = null, enabled = enabled)
RadioButton(selected = selected, onClick = null)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.padding(start = 16.dp),
) {
Text(
text = quality.label,
style = MaterialTheme.typography.bodyLarge,
color = if (enabled) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = quality.summary,
Expand All @@ -113,21 +105,3 @@ private fun QualityRow(
}
}
}

/**
* Ce qu'il faut savoir sous les choix.
*
* Par défaut, que le réglage ne touche pas la file en cours : le rendu est posé
* sur chaque piste quand elle y entre, et changer d'avis en pleine écoute sans
* que rien ne bouge se lirait sinon comme une panne.
*/
internal fun note(state: StreamQualityUiState): String = when {
state.isCurrentUnavailable ->
"Ce serveur ne sait pas transcoder : ses pistes ne pourront pas être lues " +
"dans la qualité choisie. Revenez à la qualité d'origine."

state.transcodingAvailable == false ->
"Ce serveur ne sait pas transcoder : seule la qualité d'origine est disponible."

else -> "S'applique aux pistes lancées ensuite, pas à la file en cours."
}
Original file line number Diff line number Diff line change
@@ -1,102 +1,53 @@
package app.waveflow.ui.quality

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import app.waveflow.WaveFlowApp
import app.waveflow.data.PreferencesStore
import app.waveflow.data.remote.CatalogRepository
import app.waveflow.data.remote.ServerException
import app.waveflow.model.StreamQuality
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
* Ce que l'écran sait de la qualité de lecture.
*
* @property transcodingAvailable ce que le serveur a dit de sa capacité à
* transcoder, `null` tant qu'il n'a rien dit. L'inconnu ne ferme aucun choix :
* seul un refus explicite le fait, sans quoi une coupure réseau passagère
* grisait des profils que le serveur sert très bien.
* Tous les profils se choisissent, sans demander au serveur s'il sait
* transcoder : `waveflow-server` refuse de démarrer sans ffmpeg, et un serveur
* joignable le sait donc toujours.
*/
data class StreamQualityUiState(
val current: StreamQuality = StreamQuality.Original,
val transcodingAvailable: Boolean? = null,
) {
/** L'original se sert toujours ; le reste suppose un serveur qui transcode. */
fun isSelectable(quality: StreamQuality): Boolean =
quality.rendering.isOriginal || transcodingAvailable != false

/**
* Le choix enregistré ne peut pas être servi par ce serveur.
*
* Le cas se présente : la préférence appartient à l'application, pas au
* serveur, et elle survit à un changement de serveur.
*/
val isCurrentUnavailable: Boolean get() = !isSelectable(current)
}
)

/** La qualité de lecture, vue et choisie depuis le compte serveur. */
class StreamQualityViewModel(
private val store: PreferencesStore,
private val catalog: CatalogRepository,
) : ViewModel() {

private val disponible = MutableStateFlow<Boolean?>(null)

/**
* `Eagerly`, comme les réglages : l'écran doit s'ouvrir sur le choix
* enregistré, pas sur le défaut qu'il remplacerait sous les yeux.
*/
val state: StateFlow<StreamQualityUiState> = combine(store.preferences, disponible) { prefs, dispo ->
StreamQualityUiState(current = prefs.streamQuality, transcodingAvailable = dispo)
}.stateIn(viewModelScope, SharingStarted.Eagerly, StreamQualityUiState())

private var releve: Job? = null
val state: StateFlow<StreamQualityUiState> = store.preferences
.map { prefs -> StreamQualityUiState(current = prefs.streamQuality) }
.stateIn(viewModelScope, SharingStarted.Eagerly, StreamQualityUiState())

/**
* À l'ouverture de l'écran : ffmpeg peut avoir été installé, ou retiré,
* depuis la dernière fois.
*
* Un relevé en échec laisse ce qu'on savait déjà. Un serveur injoignable
* n'a rien appris de neuf sur sa capacité à transcoder.
*/
fun refresh() {
releve?.cancel()
releve = viewModelScope.launch {
try {
disponible.value = catalog.transcodingAvailable()
} catch (error: ServerException) {
Log.w(TAG, "Capacité de transcodage inconnue", error)
}
}
}

/** Un profil que le serveur a déclaré ne pas servir n'est pas enregistré. */
fun choose(quality: StreamQuality) {
if (!state.value.isSelectable(quality)) return
viewModelScope.launch { store.setStreamQuality(quality) }
}

companion object {
private const val TAG = "StreamQualityViewModel"

val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val app = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
as WaveFlowApp
StreamQualityViewModel(
store = app.container.preferencesStore,
catalog = app.container.catalogRepository,
)
StreamQualityViewModel(store = app.container.preferencesStore)
}
}
}
Expand Down
14 changes: 0 additions & 14 deletions app/src/test/java/app/waveflow/data/remote/HttpCatalogApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -227,20 +227,6 @@ class HttpCatalogApiTest {
assertEquals("${url()}/api/v2/stream/VkdLrczM?format=opus&bitrate=96", streamUrl)
}

@Test
fun `un serveur sans ffmpeg le dit`() = runTest {
server.enqueue(
MockResponse().setBody("""{"available":false,"active":0,"global_limit":4,"per_user_limit":2}"""),
)

val disponible = api.transcodingAvailable(url(), "wfa_1")

val request = server.takeRequest()
assertEquals("/api/v2/transcode/status", request.path)
assertEquals("Bearer wfa_1", request.getHeader("Authorization"))
assertEquals(false, disponible)
}

@Test
fun `un jeton refuse remonte comme tel`() = runTest {
server.enqueue(
Expand Down
10 changes: 0 additions & 10 deletions app/src/test/java/app/waveflow/testing/ServerFakes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,6 @@ class FakeCatalogApi(
var lastRendering: StreamRendering? = null
private set

/** Ce que le serveur répond sur sa capacité à transcoder. */
var transcoding = true

override suspend fun transcodingAvailable(serverUrl: String, accessToken: String): Boolean {
record(serverUrl, accessToken, null)
return transcoding
}

private suspend fun record(serverUrl: String, accessToken: String, page: Pair<Int, Int>?) {
calls++
lastServerUrl = serverUrl
Expand Down Expand Up @@ -326,8 +318,6 @@ class PagingCatalogApi(
rendering: StreamRendering,
): String = "$serverUrl/api/v2/stream/ticket-$trackId"

override suspend fun transcodingAvailable(serverUrl: String, accessToken: String): Boolean = true

private fun failIfDue(call: Int) {
if (failFromCall > 0 && call >= failFromCall) {
throw ServerException.Unreachable("coupure")
Expand Down

This file was deleted.

Loading