feat(player): переключатель качества звука на экране воспроизведения
Перепроверены все 594 рабочие станции на наличие битрейт-вариантов потока (скрипт-пробер). У 71 станции найдено по 2–4 качества (Record-флагманы 96/64/32, zaycev 256/128/48, ВГТРК 192/128/64, НАШЕ/Орфей/Шансон HQ и др.) — записаны в поле qualities в stations.json. HLS (EMG) и Love (UID-привязка) корректно пропущены. Клиент: модель StreamQuality, хранение в Room (миграция v5), предпочтение битрейта в настройках. На экране плеера — чип текущего качества (виден только если вариантов ≥2) и шторка «Качество звука» со ступенями; переключение на лету без сброса now-playing, выбор запоминается между станциями. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,15 @@ val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE stations ADD COLUMN qualities TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
|
||||
@Database(
|
||||
entities = [StationEntity::class, TrackHistoryEntity::class, TagEntity::class, RecordingEntity::class],
|
||||
version = 4
|
||||
version = 5
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun stationDao(): StationDao
|
||||
|
||||
@@ -41,7 +41,10 @@ class LocalStationDataSource @Inject constructor(
|
||||
genre = group?.name ?: "",
|
||||
tags = listOfNotNull(group?.name?.takeIf { it.isNotBlank() }),
|
||||
sortOrder = dto.id,
|
||||
source = if (isRecord) "record" else "local"
|
||||
source = if (isRecord) "record" else "local",
|
||||
qualities = dto.qualities.orEmpty().map {
|
||||
com.radiola.domain.model.StreamQuality(it.bitrate, it.url, it.type)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,15 @@ data class LocalStationDto(
|
||||
@SerialName("bgColor") val bgColor: String? = null,
|
||||
@SerialName("enabled") val enabled: Boolean = true,
|
||||
@SerialName("notWorked") val notWorked: Boolean = false,
|
||||
@SerialName("isNew") val isNew: Boolean = false
|
||||
@SerialName("isNew") val isNew: Boolean = false,
|
||||
@SerialName("qualities") val qualities: List<LocalQualityDto>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LocalQualityDto(
|
||||
@SerialName("bitrate") val bitrate: Int,
|
||||
@SerialName("url") val url: String,
|
||||
@SerialName("type") val type: String = "aac"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -14,5 +14,7 @@ data class StationEntity(
|
||||
val tags: String,
|
||||
val sortOrder: Int,
|
||||
val source: String = "record",
|
||||
val isFavorite: Boolean = false
|
||||
val isFavorite: Boolean = false,
|
||||
// Качества потока, закодированы строкой: строки "bitrate\ttype\turl", разделённые \n.
|
||||
val qualities: String = ""
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ class SettingsRepositoryImpl @Inject constructor(
|
||||
private val ENABLED_SERVICES = stringSetPreferencesKey("enabled_deeplink_services")
|
||||
private val EQUALIZER_PRESET = stringPreferencesKey("equalizer_preset")
|
||||
private val RECORDING_ENABLED = booleanPreferencesKey("recording_enabled")
|
||||
private val PREFERRED_BITRATE = intPreferencesKey("preferred_bitrate")
|
||||
}
|
||||
|
||||
override fun getLastStationId(): Flow<Int?> = dataStore.data.map { it[LAST_STATION_ID] }
|
||||
@@ -49,4 +50,7 @@ class SettingsRepositoryImpl @Inject constructor(
|
||||
|
||||
override fun isRecordingEnabled(): Flow<Boolean> = dataStore.data.map { it[RECORDING_ENABLED] ?: false }
|
||||
override suspend fun setRecordingEnabled(enabled: Boolean) { dataStore.edit { it[RECORDING_ENABLED] = enabled } }
|
||||
|
||||
override fun getPreferredBitrate(): Flow<Int> = dataStore.data.map { it[PREFERRED_BITRATE] ?: 0 }
|
||||
override suspend fun setPreferredBitrate(bitrate: Int) { dataStore.edit { it[PREFERRED_BITRATE] = bitrate } }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.radiola.data.remote.RecordApi
|
||||
import com.radiola.data.remote.RadiolaApi
|
||||
import com.radiola.data.remote.ApiMapper.toDomain
|
||||
import com.radiola.domain.model.Station
|
||||
import com.radiola.domain.model.StreamQuality
|
||||
import com.radiola.domain.repository.StationRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -84,7 +85,8 @@ class StationRepositoryImpl @Inject constructor(
|
||||
tags = station.tags.joinToString(","),
|
||||
sortOrder = index,
|
||||
source = station.source,
|
||||
isFavorite = false
|
||||
isFavorite = false,
|
||||
qualities = encodeQualities(station.qualities)
|
||||
)
|
||||
}
|
||||
db.stationDao().insertAll(entities)
|
||||
@@ -133,6 +135,22 @@ class StationRepositoryImpl @Inject constructor(
|
||||
genre = genre,
|
||||
tags = tags.split(",").filter { it.isNotBlank() },
|
||||
sortOrder = sortOrder,
|
||||
source = source
|
||||
source = source,
|
||||
qualities = decodeQualities(qualities)
|
||||
)
|
||||
|
||||
// Качества кодируем строкой "bitrate\ttype\turl" по строкам (URL может содержать ; и |,
|
||||
// но не \t/\n — поэтому такие разделители безопасны).
|
||||
private fun encodeQualities(list: List<StreamQuality>): String =
|
||||
list.joinToString("\n") { "${it.bitrate}\t${it.type}\t${it.url}" }
|
||||
|
||||
private fun decodeQualities(raw: String): List<StreamQuality> {
|
||||
if (raw.isBlank()) return emptyList()
|
||||
return raw.split("\n").mapNotNull { line ->
|
||||
val parts = line.split("\t")
|
||||
if (parts.size != 3) return@mapNotNull null
|
||||
val br = parts[0].toIntOrNull() ?: return@mapNotNull null
|
||||
StreamQuality(br, parts[2], parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user