add format in to stream request #1

Closed
opened 2026-10-15 08:54:59 +07:00 by lisenblsh · 2 comments
Owner
interface SubsonicApi {
    ...
    companion object {
        ...
        fun getStreamUrl(
            baseUrl: String,
            id: String,
            u: String,
            t: String,
            s: String,
            maxBitRate: Int? = null,
            timeOffset: Int? = null,
            format: String? = null,
        ): String {
            var url = "${baseUrl.removeSuffix("/")}/rest/stream.view?id=$id&u=$u&t=$t&s=$s&v=1.16.1&c=LisMusic"
            if (maxBitRate != null && maxBitRate > 0) {
                url += "&maxBitRate=$maxBitRate"
            }
            if (timeOffset != null && timeOffset > 0) {
                url += "&timeOffset=$timeOffset"
            }
            if (!format.isNullOrBlank()) {
                url += "&format=$format"
            }
            return url
        }
    }
}

class UserPreferences(private val context: Context) {
    ...
    companion object {
        ...
        val MUSIC_FORMAT = stringPreferencesKey("music_format")
        ...
    }
    ...
    val musicFormat: Flow<String?> = context.dataStore.data.map { it[MUSIC_FORMAT] }
    ...
    suspend fun saveMusicFormat(format: String?) {
        context.dataStore.edit { preferences ->
            if (!format.isNullOrBlank) {
                preferences[MUSIC_FORMAT] = format
            } else {
                preferences.remove(MUSIC_FORMAT)
            }

        }
    }
    ...
}

class SubsonicViewModel(application: Application) : AndroidViewModel(application) {

    ...

    // Settings
    var maxBitRate by mutableIntStateOf(0) // 0 means no transcoding
        private set
    var musicFormat by mutableStateOf("")
        private set
    var loadImages by mutableStateOf(true)
        private set
    var jukeboxEnabled by mutableStateOf(false)
        private set
    ...
    fun updateMusicFormat(format: String?) {
        musicFormat = format
        viewModelScope.launch {
            userPreferences.saveMusicFormat(format)
        }
    }

    ...

    init {
        viewModelScope.launch {
            maxBitRate = userPreferences.maxBitrate.first()
            musicFormat = userPreferences.musicFormat.first()
            loadImages = userPreferences.loadImages.first()
            jukeboxEnabled = userPreferences.jukeboxEnabled.first()
            listenBrainzEnabled = userPreferences.listenBrainzEnabled.first()
            listenBrainzToken = userPreferences.listenBrainzToken.first() ?: ""
        }
    ...
    }
    ...
    fun seekTo(position: Long) {
        if (jukeboxEnabled) {
            playbackPosition = position
            val positionSeconds = (position / 1000).toInt()
            jukeboxControl("set", position = positionSeconds)
        } else {
            val s = session ?: return
            val controller = mediaController ?: return
            val currentItem = controller.currentMediaItem ?: return
            
            if (maxBitRate > 0) {
                val timeOffsetSeconds = (position / 1000).toInt()
                val newStreamUrl = SubsonicApi.getStreamUrl(
                    s.baseUrl, 
                    currentItem.mediaId, 
                    s.username, 
                    s.token, 
                    s.salt,
                    maxBitRate = maxBitRate,
                    musicFormat = if(!musicFormat.isNullOrBlank) musicFormat else null,
                    timeOffset = timeOffsetSeconds
                )
                
                val newMediaItem = currentItem.buildUpon()
                    .setUri(newStreamUrl)
                    .build()
                
                controller.setMediaItem(newMediaItem)
                controller.prepare()
                controller.play()
            } else {
                controller.seekTo(position)
            }
        }
    }

    ...
    private fun songToMediaItem(song: Song): MediaItem {
        val s = session ?: return MediaItem.Builder().setMediaId(song.id).build()
        val streamUrl = SubsonicApi.getStreamUrl(
            s.baseUrl, 
            song.id, 
            s.username, 
            s.token, 
            s.salt,
            maxBitRate = if (maxBitRate > 0) maxBitRate else null,
            format = if(!musicFormat.isNullOrBlank) musicFormat else null
        )
        ...
    }
    ...
}

```kotlin interface SubsonicApi { ... companion object { ... fun getStreamUrl( baseUrl: String, id: String, u: String, t: String, s: String, maxBitRate: Int? = null, timeOffset: Int? = null, format: String? = null, ): String { var url = "${baseUrl.removeSuffix("/")}/rest/stream.view?id=$id&u=$u&t=$t&s=$s&v=1.16.1&c=LisMusic" if (maxBitRate != null && maxBitRate > 0) { url += "&maxBitRate=$maxBitRate" } if (timeOffset != null && timeOffset > 0) { url += "&timeOffset=$timeOffset" } if (!format.isNullOrBlank()) { url += "&format=$format" } return url } } } class UserPreferences(private val context: Context) { ... companion object { ... val MUSIC_FORMAT = stringPreferencesKey("music_format") ... } ... val musicFormat: Flow<String?> = context.dataStore.data.map { it[MUSIC_FORMAT] } ... suspend fun saveMusicFormat(format: String?) { context.dataStore.edit { preferences -> if (!format.isNullOrBlank) { preferences[MUSIC_FORMAT] = format } else { preferences.remove(MUSIC_FORMAT) } } } ... } class SubsonicViewModel(application: Application) : AndroidViewModel(application) { ... // Settings var maxBitRate by mutableIntStateOf(0) // 0 means no transcoding private set var musicFormat by mutableStateOf("") private set var loadImages by mutableStateOf(true) private set var jukeboxEnabled by mutableStateOf(false) private set ... fun updateMusicFormat(format: String?) { musicFormat = format viewModelScope.launch { userPreferences.saveMusicFormat(format) } } ... init { viewModelScope.launch { maxBitRate = userPreferences.maxBitrate.first() musicFormat = userPreferences.musicFormat.first() loadImages = userPreferences.loadImages.first() jukeboxEnabled = userPreferences.jukeboxEnabled.first() listenBrainzEnabled = userPreferences.listenBrainzEnabled.first() listenBrainzToken = userPreferences.listenBrainzToken.first() ?: "" } ... } ... fun seekTo(position: Long) { if (jukeboxEnabled) { playbackPosition = position val positionSeconds = (position / 1000).toInt() jukeboxControl("set", position = positionSeconds) } else { val s = session ?: return val controller = mediaController ?: return val currentItem = controller.currentMediaItem ?: return if (maxBitRate > 0) { val timeOffsetSeconds = (position / 1000).toInt() val newStreamUrl = SubsonicApi.getStreamUrl( s.baseUrl, currentItem.mediaId, s.username, s.token, s.salt, maxBitRate = maxBitRate, musicFormat = if(!musicFormat.isNullOrBlank) musicFormat else null, timeOffset = timeOffsetSeconds ) val newMediaItem = currentItem.buildUpon() .setUri(newStreamUrl) .build() controller.setMediaItem(newMediaItem) controller.prepare() controller.play() } else { controller.seekTo(position) } } } ... private fun songToMediaItem(song: Song): MediaItem { val s = session ?: return MediaItem.Builder().setMediaId(song.id).build() val streamUrl = SubsonicApi.getStreamUrl( s.baseUrl, song.id, s.username, s.token, s.salt, maxBitRate = if (maxBitRate > 0) maxBitRate else null, format = if(!musicFormat.isNullOrBlank) musicFormat else null ) ... } ... } ```
Author
Owner

UI - SettingsScreen.kt

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    viewModel: SubsonicViewModel,
    onBackClick: () -> Unit
) {
    ...
    val formats = listOf(
        null to "none",
        "aac" to "aac",
        "flac" to "flac",
        "opus" to "opus",
        "mp3" to "mp3"
    )
    ...
                // Format
                Text(text = "Format (Transcoding)", style = MaterialTheme.typography.bodyMedium)
                Box(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
                    OutlinedCard(
                        onClick = { expanded = true },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text(
                            text = formats.find { it.first == viewModel.musicFormat }?.second ?: "none",
                            modifier = Modifier.padding(16.dp)
                        )
                    }
                    DropdownMenu(
                        expanded = expanded,
                        onDismissRequest = { expanded = false },
                        modifier = Modifier.fillMaxWidth(0.9f)
                    ) {
                        formats.forEach { (value, label) ->
                            DropdownMenuItem(
                                text = { Text(label) },
                                onClick = {
                                    viewModel.updateMusicFormat(value)
                                    expanded = false
                                }
                            )
                        }
                    }
                }
    ...
    
}
UI - SettingsScreen.kt ```kotlin @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( viewModel: SubsonicViewModel, onBackClick: () -> Unit ) { ... val formats = listOf( null to "none", "aac" to "aac", "flac" to "flac", "opus" to "opus", "mp3" to "mp3" ) ... // Format Text(text = "Format (Transcoding)", style = MaterialTheme.typography.bodyMedium) Box(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) { OutlinedCard( onClick = { expanded = true }, modifier = Modifier.fillMaxWidth() ) { Text( text = formats.find { it.first == viewModel.musicFormat }?.second ?: "none", modifier = Modifier.padding(16.dp) ) } DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier.fillMaxWidth(0.9f) ) { formats.forEach { (value, label) -> DropdownMenuItem( text = { Text(label) }, onClick = { viewModel.updateMusicFormat(value) expanded = false } ) } } } ... } ```
Author
Owner
77081a5d07d581761d14ad701ed5d42210d4f2b5
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lisenblsh/LisMusic#1
No description provided.