fix(now-playing): матч текущего трека по id станции, а не по имени
Станции с одинаковым именем в разных сетях (напр. «Deep» у Record и DFM) показывали один и тот же трек — матч был по lowercase-имени. Каталожный id (== station.id) уникален и совпадает со stationId в /now-playing, поэтому матчим по id. Убран весь by-name путь (репозиторий, плеер, карточки). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,27 +16,16 @@ class NowPlayingRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
private val _nowPlaying = MutableStateFlow<Map<Int, Track>>(emptyMap())
|
private val _nowPlaying = MutableStateFlow<Map<Int, Track>>(emptyMap())
|
||||||
|
|
||||||
// Карта по lowercase-имени станции: заполняется при REST-поллинге,
|
|
||||||
// используется для матчинга карточек (id локальных станций может отличаться).
|
|
||||||
private val _nowPlayingByName = MutableStateFlow<Map<String, Track>>(emptyMap())
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
socketClient.connect()
|
socketClient.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сокет (реалтайм, приоритет) + REST-поллинг с нашего бэкенда.
|
// Сокет (реалтайм, приоритет) + REST-поллинг с нашего бэкенда.
|
||||||
// Оба источника ключуются по числовому id станции (как в каталоге),
|
// Оба источника ключуются по числовому id станции каталога (== station.id),
|
||||||
// поэтому корректно сопоставляются с station.id плеера.
|
// поэтому матчатся однозначно — без коллизий по одинаковым названиям станций.
|
||||||
override fun getNowPlaying(stationId: Int, stationName: String): Flow<Track?> {
|
override fun getNowPlaying(stationId: Int): Flow<Track?> {
|
||||||
val nameKey = stationName.trim().lowercase()
|
return combine(socketClient.nowPlaying, _nowPlaying) { socketMap, restMap ->
|
||||||
return combine(
|
socketMap[stationId] ?: restMap[stationId]
|
||||||
socketClient.nowPlaying,
|
|
||||||
_nowPlaying,
|
|
||||||
_nowPlayingByName
|
|
||||||
) { socketMap, restMap, byName ->
|
|
||||||
// Числовой id (сокет/REST), затем фолбэк по имени — id локальных
|
|
||||||
// станций (DFM и др.) не совпадает с каталожным, имя совпадает.
|
|
||||||
socketMap[stationId] ?: restMap[stationId] ?: byName[nameKey]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +34,8 @@ class NowPlayingRepositoryImpl @Inject constructor(
|
|||||||
restMap + socketMap
|
restMap + socketMap
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllNowPlayingByName(): Flow<Map<String, Track>> = _nowPlayingByName
|
|
||||||
|
|
||||||
override suspend fun refreshNowPlaying(): Result<Unit> {
|
override suspend fun refreshNowPlaying(): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
// Берём now-playing с нашего бэкенда: там корректный маппинг
|
|
||||||
// now-слотов Record -> id станций (recordSync). Сырой Record-эндпоинт
|
|
||||||
// использует id now-слотов, которые не совпадают с id каталога.
|
|
||||||
val list = radiolaApi.getNowPlaying()
|
val list = radiolaApi.getNowPlaying()
|
||||||
_nowPlaying.value = list.associate { dto ->
|
_nowPlaying.value = list.associate { dto ->
|
||||||
dto.stationId to Track(
|
dto.stationId to Track(
|
||||||
@@ -61,17 +45,6 @@ class NowPlayingRepositoryImpl @Inject constructor(
|
|||||||
stationName = dto.name
|
stationName = dto.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Параллельный индекс по имени — для матчинга карточек станций.
|
|
||||||
_nowPlayingByName.value = list
|
|
||||||
.filter { it.name.isNotBlank() }
|
|
||||||
.associate { dto ->
|
|
||||||
dto.name.trim().lowercase() to Track(
|
|
||||||
artist = dto.artist,
|
|
||||||
song = dto.song,
|
|
||||||
coverUrl = dto.coverUrl,
|
|
||||||
stationName = dto.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import com.radiola.domain.model.Track
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface NowPlayingRepository {
|
interface NowPlayingRepository {
|
||||||
fun getNowPlaying(stationId: Int, stationName: String): Flow<Track?>
|
fun getNowPlaying(stationId: Int): Flow<Track?>
|
||||||
fun getAllNowPlaying(): Flow<Map<Int, Track>>
|
fun getAllNowPlaying(): Flow<Map<Int, Track>>
|
||||||
// Карта по lowercase-имени станции — для матчинга с карточками (id может не совпадать).
|
|
||||||
fun getAllNowPlayingByName(): Flow<Map<String, Track>>
|
|
||||||
suspend fun refreshNowPlaying(): Result<Unit>
|
suspend fun refreshNowPlaying(): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import javax.inject.Inject
|
|||||||
class GetNowPlayingUseCase @Inject constructor(
|
class GetNowPlayingUseCase @Inject constructor(
|
||||||
private val nowPlayingRepository: NowPlayingRepository
|
private val nowPlayingRepository: NowPlayingRepository
|
||||||
) {
|
) {
|
||||||
operator fun invoke(stationId: Int, stationName: String): Flow<Track?> {
|
operator fun invoke(stationId: Int): Flow<Track?> {
|
||||||
return nowPlayingRepository.getNowPlaying(stationId, stationName)
|
return nowPlayingRepository.getNowPlaying(stationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun FavoritesScreen(
|
|||||||
isFavorite = favoriteIds.contains(station.id),
|
isFavorite = favoriteIds.contains(station.id),
|
||||||
onClick = { onStationClick(station) },
|
onClick = { onStationClick(station) },
|
||||||
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
||||||
nowTrack = nowPlaying[station.name.trim().lowercase()],
|
nowTrack = nowPlaying[station.id],
|
||||||
modifier = Modifier.animateItemPlacement()
|
modifier = Modifier.animateItemPlacement()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class FavoritesViewModel @Inject constructor(
|
|||||||
.map { list -> list.map { it.id }.toSet() }
|
.map { list -> list.map { it.id }.toSet() }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
|
||||||
|
|
||||||
// Текущие треки по lowercase-имени станции — для обложек на карточках.
|
// Текущие треки по id станции (каталожный == station.id) — без коллизий по имени.
|
||||||
val nowPlaying: StateFlow<Map<String, Track>> = nowPlayingRepository.getAllNowPlayingByName()
|
val nowPlaying: StateFlow<Map<Int, Track>> = nowPlayingRepository.getAllNowPlaying()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class PlayerViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
// Collect now playing for this station (API has priority: covers + accurate metadata)
|
// Collect now playing for this station (API has priority: covers + accurate metadata)
|
||||||
launch {
|
launch {
|
||||||
getNowPlayingUseCase(station.id, station.name)
|
getNowPlayingUseCase(station.id)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { track ->
|
.collect { track ->
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ fun StationsScreen(
|
|||||||
isFavorite = favoriteIds.contains(station.id),
|
isFavorite = favoriteIds.contains(station.id),
|
||||||
onClick = { onStationClick(station) },
|
onClick = { onStationClick(station) },
|
||||||
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
||||||
nowTrack = nowPlaying[station.name.trim().lowercase()],
|
nowTrack = nowPlaying[station.id],
|
||||||
modifier = Modifier.animateItemPlacement()
|
modifier = Modifier.animateItemPlacement()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ class StationsViewModel @Inject constructor(
|
|||||||
.map { list -> list.map { it.id }.toSet() }
|
.map { list -> list.map { it.id }.toSet() }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
|
||||||
|
|
||||||
// Текущие треки по lowercase-имени станции — для обложек на карточках.
|
// Текущие треки по id станции (каталожный == station.id) — без коллизий по имени.
|
||||||
val nowPlaying: StateFlow<Map<String, Track>> = nowPlayingRepository.getAllNowPlayingByName()
|
val nowPlaying: StateFlow<Map<Int, Track>> = nowPlayingRepository.getAllNowPlaying()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
Reference in New Issue
Block a user