Refresh programs for series recordings

This commit is contained in:
Damontecres 2025-10-21 13:46:36 -04:00
parent fd853acdd5
commit 0438b889b6
No known key found for this signature in database
3 changed files with 94 additions and 87 deletions

View file

@ -1,7 +0,0 @@
package com.github.damontecres.wholphin.ui.detail.livetv
import androidx.compose.runtime.Composable
@Composable
fun ChannelRow() {
}

View file

@ -48,6 +48,7 @@ class LiveTvViewModel
val start = val start =
LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
private lateinit var channelsIdToIndex: Map<UUID, Int>
val channels = MutableLiveData<List<TvChannel>>() val channels = MutableLiveData<List<TvChannel>>()
val programs = MutableLiveData<List<TvProgram>>() val programs = MutableLiveData<List<TvProgram>>()
@ -75,91 +76,94 @@ class LiveTvViewModel
) )
} }
Timber.d("Got ${channels.size} channels") Timber.d("Got ${channels.size} channels")
val channelsIdToIndex = channelsIdToIndex =
channels.withIndex().associateBy({ it.value.id }, { it.index }) channels.withIndex().associateBy({ it.value.id }, { it.index })
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1) fetchPrograms(channels)
val minEndDate = start.plusMinutes(1L)
val request =
GetProgramsDto(
maxStartDate = maxStartDate,
minEndDate = minEndDate,
// maxEndDate = start.plusHours(25),
channelIds = channels.map { it.id },
sortBy = listOf(ItemSortBy.START_DATE),
)
// val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init()
// (0..<pager.size).forEach { pager.getBlocking(it) }
val programs =
GetProgramsDtoHandler
.execute(api, request)
.content.items
.map { dto ->
TvProgram(
id = dto.id,
channelId = dto.channelId!!,
start = dto.startDate!!,
end = dto.endDate!!,
startHours = hoursBetween(start, dto.startDate!!),
endHours = hoursBetween(start, dto.endDate!!),
duration = dto.runTimeTicks!!.ticks,
name = dto.seriesName ?: dto.name,
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
seasonEpisode =
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
} else {
null
},
isRecording = dto.timerId.isNotNullOrBlank(),
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
)
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
val programsByChannel = programs.groupBy { it.channelId }
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
val fake = mutableListOf<TvProgram>()
val finalProgramsByChannel =
programsByChannel.toMutableMap().apply {
emptyChannels.forEach { channel ->
val fakePrograms =
(0..<MAX_HOURS).map {
TvProgram(
id = UUID.randomUUID(), // TODO
channelId = channel.id,
start = start.plusHours(it),
end = start.plusHours(it + 1),
startHours = it.toFloat(),
endHours = (it + 1).toFloat(),
duration = 60.seconds,
name = "No data",
subtitle = null,
seasonEpisode = null,
isRecording = false,
isSeriesRecording = false,
)
}
put(channel.id, 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) { withContext(Dispatchers.Main) {
this@LiveTvViewModel.channels.value = channels this@LiveTvViewModel.channels.value = channels
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
loading.value = LoadingState.Success loading.value = LoadingState.Success
} }
} }
} }
private suspend fun fetchPrograms(channels: List<TvChannel>) {
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
val minEndDate = start.plusMinutes(1L)
val request =
GetProgramsDto(
maxStartDate = maxStartDate,
minEndDate = minEndDate,
channelIds = channels.map { it.id },
sortBy = listOf(ItemSortBy.START_DATE),
)
val programs =
GetProgramsDtoHandler
.execute(api, request)
.content.items
.map { dto ->
TvProgram(
id = dto.id,
channelId = dto.channelId!!,
start = dto.startDate!!,
end = dto.endDate!!,
startHours = hoursBetween(start, dto.startDate!!),
endHours = hoursBetween(start, dto.endDate!!),
duration = dto.runTimeTicks!!.ticks,
name = dto.seriesName ?: dto.name,
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
seasonEpisode =
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
} else {
null
},
isRecording = dto.timerId.isNotNullOrBlank(),
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
)
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
val programsByChannel = programs.groupBy { it.channelId }
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
val fake = mutableListOf<TvProgram>()
val finalProgramsByChannel =
programsByChannel.toMutableMap().apply {
emptyChannels.forEach { channel ->
val fakePrograms =
(0..<MAX_HOURS).map {
TvProgram(
id = UUID.randomUUID(), // TODO
channelId = channel.id,
start = start.plusHours(it),
end = start.plusHours(it + 1),
startHours = it.toFloat(),
endHours = (it + 1).toFloat(),
duration = 60.seconds,
name = "No data",
subtitle = null,
seasonEpisode = null,
isRecording = false,
isSeriesRecording = false,
)
}
put(channel.id, 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
}
}
fun getItem(programId: UUID) { fun getItem(programId: UUID) {
fetchingItem.value = LoadingState.Loading fetchingItem.value = LoadingState.Loading
viewModelScope.launchIO(LoadingExceptionHandler(fetchingItem, "Error")) { viewModelScope.launchIO(LoadingExceptionHandler(fetchingItem, "Error")) {
@ -172,6 +176,13 @@ class LiveTvViewModel
fetchedItem.value = result fetchedItem.value = result
fetchingItem.value = LoadingState.Success fetchingItem.value = LoadingState.Success
} }
if (result.data.seriesTimerId != null) {
val items =
api.liveTvApi
.getPrograms(GetProgramsDto(seriesTimerId = result.data.seriesTimerId))
.content.items
Timber.v("items=$items")
}
} }
} }
@ -185,10 +196,11 @@ class LiveTvViewModel
viewModelScope.launch(ExceptionHandler(autoToast = true)) { viewModelScope.launch(ExceptionHandler(autoToast = true)) {
if (series) { if (series) {
api.liveTvApi.cancelSeriesTimer(timerId) api.liveTvApi.cancelSeriesTimer(timerId)
fetchPrograms(channels.value.orEmpty())
} else { } else {
api.liveTvApi.cancelTimer(timerId) api.liveTvApi.cancelTimer(timerId)
refreshProgram(programIndex, programId)
} }
refreshProgram(programIndex, programId)
} }
} }
} }
@ -202,6 +214,7 @@ class LiveTvViewModel
val d by api.liveTvApi.getDefaultTimer(programId.toServerString()) val d by api.liveTvApi.getDefaultTimer(programId.toServerString())
if (series) { if (series) {
api.liveTvApi.createSeriesTimer(d) api.liveTvApi.createSeriesTimer(d)
fetchPrograms(channels.value.orEmpty())
} else { } else {
val payload = val payload =
TimerInfoDto( TimerInfoDto(
@ -227,8 +240,8 @@ class LiveTvViewModel
keepUntil = d.keepUntil, keepUntil = d.keepUntil,
) )
api.liveTvApi.createTimer(payload) api.liveTvApi.createTimer(payload)
refreshProgram(programIndex, programId)
} }
refreshProgram(programIndex, programId)
} }
} }

View file

@ -559,12 +559,13 @@ fun TvGuideGrid(
} }
} }
if (program.isSeriesRecording) { if (program.isSeriesRecording) {
val color = if (program.isRecording) Color.Red else Color.Gray
Box( Box(
modifier = modifier =
Modifier Modifier
.padding(4.dp) .padding(4.dp)
.size(16.dp) .size(16.dp)
.background(Color.Red.copy(alpha = .5f), shape = CircleShape) .background(color.copy(alpha = .5f), shape = CircleShape)
.align(Alignment.BottomEnd), .align(Alignment.BottomEnd),
) )
Box( Box(
@ -572,7 +573,7 @@ fun TvGuideGrid(
Modifier Modifier
.padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 10.dp) .padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 10.dp)
.size(16.dp) .size(16.dp)
.background(Color.Red, shape = CircleShape) .background(color, shape = CircleShape)
.align(Alignment.BottomEnd), .align(Alignment.BottomEnd),
) )
} else if (program.isRecording) { } else if (program.isRecording) {