Add locking for refreshes

This commit is contained in:
Damontecres 2025-10-21 13:49:46 -04:00
parent 0438b889b6
commit 724519a6fa
No known key found for this signature in database

View file

@ -17,6 +17,8 @@ import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.api.client.extensions.imageApi
@ -49,6 +51,7 @@ class LiveTvViewModel
val start = val start =
LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
private lateinit var channelsIdToIndex: Map<UUID, Int> private lateinit var channelsIdToIndex: Map<UUID, Int>
private val mutex = Mutex()
val channels = MutableLiveData<List<TvChannel>>() val channels = MutableLiveData<List<TvChannel>>()
val programs = MutableLiveData<List<TvProgram>>() val programs = MutableLiveData<List<TvProgram>>()
@ -87,82 +90,83 @@ class LiveTvViewModel
} }
} }
private suspend fun fetchPrograms(channels: List<TvChannel>) { private suspend fun fetchPrograms(channels: List<TvChannel>) =
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1) mutex.withLock {
val minEndDate = start.plusMinutes(1L) val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
val request = val minEndDate = start.plusMinutes(1L)
GetProgramsDto( val request =
maxStartDate = maxStartDate, GetProgramsDto(
minEndDate = minEndDate, maxStartDate = maxStartDate,
channelIds = channels.map { it.id }, minEndDate = minEndDate,
sortBy = listOf(ItemSortBy.START_DATE), channelIds = channels.map { it.id },
) sortBy = listOf(ItemSortBy.START_DATE),
val programs = )
GetProgramsDtoHandler val programs =
.execute(api, request) GetProgramsDtoHandler
.content.items .execute(api, request)
.map { dto -> .content.items
TvProgram( .map { dto ->
id = dto.id, TvProgram(
channelId = dto.channelId!!, id = dto.id,
start = dto.startDate!!, channelId = dto.channelId!!,
end = dto.endDate!!, start = dto.startDate!!,
startHours = hoursBetween(start, dto.startDate!!), end = dto.endDate!!,
endHours = hoursBetween(start, dto.endDate!!), startHours = hoursBetween(start, dto.startDate!!),
duration = dto.runTimeTicks!!.ticks, endHours = hoursBetween(start, dto.endDate!!),
name = dto.seriesName ?: dto.name, duration = dto.runTimeTicks!!.ticks,
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false }, name = dto.seriesName ?: dto.name,
seasonEpisode = subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
if (dto.indexNumber != null && dto.parentIndexNumber != null) { seasonEpisode =
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!) if (dto.indexNumber != null && dto.parentIndexNumber != null) {
} else { SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
null } else {
}, null
isRecording = dto.timerId.isNotNullOrBlank(), },
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(), isRecording = dto.timerId.isNotNullOrBlank(),
) isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side )
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
val programsByChannel = programs.groupBy { it.channelId } val programsByChannel = programs.groupBy { it.channelId }
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() } val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
val fake = mutableListOf<TvProgram>() val fake = mutableListOf<TvProgram>()
val finalProgramsByChannel = val finalProgramsByChannel =
programsByChannel.toMutableMap().apply { programsByChannel.toMutableMap().apply {
emptyChannels.forEach { channel -> emptyChannels.forEach { channel ->
val fakePrograms = val fakePrograms =
(0..<MAX_HOURS).map { (0..<MAX_HOURS).map {
TvProgram( TvProgram(
id = UUID.randomUUID(), // TODO id = UUID.randomUUID(), // TODO
channelId = channel.id, channelId = channel.id,
start = start.plusHours(it), start = start.plusHours(it),
end = start.plusHours(it + 1), end = start.plusHours(it + 1),
startHours = it.toFloat(), startHours = it.toFloat(),
endHours = (it + 1).toFloat(), endHours = (it + 1).toFloat(),
duration = 60.seconds, duration = 60.seconds,
name = "No data", name = "No data",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
isRecording = false, isRecording = false,
isSeriesRecording = false, isSeriesRecording = false,
) )
} }
put(channel.id, fakePrograms) put(channel.id, fakePrograms)
fake.addAll(fakePrograms) fake.addAll(fakePrograms)
}
} }
val finalProgramList =
(programs + fake).sortedWith(
compareBy(
{ channelsIdToIndex[it.channelId]!! },
{ it.start },
),
)
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
} }
val finalProgramList =
(programs + fake).sortedWith(
compareBy(
{ channelsIdToIndex[it.channelId]!! },
{ it.start },
),
)
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
} }
}
fun getItem(programId: UUID) { fun getItem(programId: UUID) {
fetchingItem.value = LoadingState.Loading fetchingItem.value = LoadingState.Loading
@ -248,7 +252,7 @@ class LiveTvViewModel
suspend fun refreshProgram( suspend fun refreshProgram(
programIndex: Int, programIndex: Int,
programId: UUID, programId: UUID,
) { ) = mutex.withLock {
val program by api.liveTvApi.getProgram(programId.toServerString()) val program by api.liveTvApi.getProgram(programId.toServerString())
val newProgram = val newProgram =
programs.value?.getOrNull(programIndex)?.copy( programs.value?.getOrNull(programIndex)?.copy(