From 88e3a2f77d4c3b940a2519647c305a2e98b7712c Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 3 Nov 2025 19:00:00 -0500 Subject: [PATCH] Fill missing guide data items --- .../ui/detail/livetv/LiveTvViewModel.kt | 242 +++++++++++++----- .../wholphin/ui/detail/livetv/TvGuideGrid.kt | 2 +- 2 files changed, 183 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index 1f2c9963..e8040056 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.WholphinApplication import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.data.RowColumn @@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.api.client.extensions.liveTvApi +import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.GetProgramsDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy @@ -40,7 +42,9 @@ import java.time.LocalDateTime import java.time.temporal.ChronoUnit import java.util.UUID import javax.inject.Inject +import kotlin.math.min import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds const val MAX_HOURS = 48L @@ -55,7 +59,7 @@ class LiveTvViewModel ) : ViewModel() { val loading = MutableLiveData(LoadingState.Pending) - lateinit var start: LocalDateTime + lateinit var guideStart: LocalDateTime private set private lateinit var channelsIdToIndex: Map private val mutex = Mutex() @@ -67,10 +71,10 @@ class LiveTvViewModel val fetchingItem = MutableLiveData(LoadingState.Pending) val fetchedItem = MutableLiveData(null) - private val range = 150 + private val range = 100 fun init(firstLoad: Boolean) { - start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) + guideStart = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) if (!firstLoad) { loading.value = LoadingState.Loading } @@ -99,12 +103,19 @@ class LiveTvViewModel Timber.d("Got ${channels.size} channels") channelsIdToIndex = channels.withIndex().associateBy({ it.value.id }, { it.index }) - fetchPrograms(channels, 0.. initial) { + fetchPrograms(channels, 0.., range: IntRange, ) = mutex.withLock { - val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1) - val minEndDate = start.plusMinutes(1L) + val maxStartDate = guideStart.plusHours(MAX_HOURS).minusMinutes(1) + val minEndDate = guideStart.plusMinutes(1L) val channelsToFetch = channels.subList(range.first, range.last + 1) Timber.v("Fetching programs for $range channels ${channelsToFetch.size}") val request = @@ -132,11 +143,17 @@ class LiveTvViewModel channelIds = channelsToFetch.map { it.id }, sortBy = listOf(ItemSortBy.START_DATE), ) - val programs = + val fetchedPrograms = api.liveTvApi .getPrograms(request) .content.items - .map { dto -> + val programsByChannel = mutableMapOf>() + val fetchedGroupedBy = fetchedPrograms.groupBy { it.channelId } + fetchedGroupedBy.forEach { (channelId, programs) -> + if (channelId != null) { + val listing = mutableListOf() + programs as MutableList + programs.forEachIndexed { index, dto -> val category = if (dto.isKids ?: false) { ProgramCategory.KIDS @@ -149,74 +166,153 @@ class LiveTvViewModel } else { null } - TvProgram( - id = dto.id, - channelId = dto.channelId!!, - start = dto.startDate!!, - end = dto.endDate!!, - startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f), - 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(), - isRepeat = dto.isRepeat ?: false, - category = category, - ) + val p = + TvProgram( + id = dto.id, + channelId = dto.channelId!!, + start = dto.startDate!!, + end = dto.endDate!!, + startHours = + hoursBetween( + guideStart, + dto.startDate!!, + ).coerceAtLeast(0f), + endHours = hoursBetween(guideStart, 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(), + isRepeat = dto.isRepeat ?: false, + category = category, + ) + if (index == 0) { + if (p.startHours > 0) { + // Fill out before the first program + var start = 0f + var end = min(start + 1f, p.startHours) + while (start < p.startHours) { + val fake = + TvProgram.fake( + guideStart, + channelId, + start, + end, + ) + start = end + end = min(start + 1f, p.startHours) + listing.add(fake) + } + } + listing.add(p) + } else if (index > 0) { + if (channelId == UUID.fromString("638232b7-7ef7-7e98-903d-2249ee3fd2cd")) { + Timber.v("Found ") + } + var previous = listing.last() + while (previous.endHours < p.startHours) { + // Fill gaps between programs + val start = previous.endHours + val duration = (p.startHours - start).coerceAtMost(1f) +// Timber.v("Adding fake for $channelId: $start=>${start + duration}") + previous = + TvProgram( + id = UUID.randomUUID(), + channelId = channelId, + start = previous.end, + end = previous.end.plusMinutes((duration * 60).toLong()), + startHours = start, + endHours = start + duration, + duration = (duration * 60).toInt().minutes, + name = context.getString(R.string.no_data), + subtitle = null, + seasonEpisode = null, + isRecording = false, + isSeriesRecording = false, + isRepeat = false, + category = ProgramCategory.FAKE, + ) + listing.add(previous) + } + listing.add(p) + } + if (index == (programs.size - 1)) { + if (p.endHours < MAX_HOURS) { + // Fill after the last program + val end = (p.endHours + 1).toInt() + listing.add( + TvProgram.fake( + guideStart, + channelId, + p.endHours, + end.toFloat(), + ), + ) + (end..() - val finalProgramsByChannel = - programsByChannel.toMutableMap().apply { - emptyChannels.forEach { channel -> - val fakePrograms = - (0.. + val fakePrograms = + (0.. + programsByChannel.put(channel.id, fakePrograms) + fake.addAll(fakePrograms) + } + + programsByChannel.forEach { (channelId, programs) -> channelProgramCount[channelId] = programs.size } val finalProgramList = - (programs + fake) + (programsByChannel.values.flatten()) .sortedWith( compareBy( { channelsIdToIndex[it.channelId]!! }, { it.start }, ), ) - Timber.d("Got ${programs.size} programs & ${fake.size} fake programs") + Timber.d("Got ${fetchedPrograms.size} programs & ${finalProgramList.size} total programs") withContext(Dispatchers.Main) { this@LiveTvViewModel.programs.value = - FetchedPrograms(range, finalProgramList, finalProgramsByChannel) + FetchedPrograms(range, finalProgramList, programsByChannel) } } @@ -378,6 +474,32 @@ data class TvProgram( val category: ProgramCategory?, ) { val isFake = category == ProgramCategory.FAKE + + companion object { + private val NO_DATA = WholphinApplication.instance.getString(R.string.no_data) + + fun fake( + zeroHourStart: LocalDateTime, + channelId: UUID, + startHours: Float, + endHours: Float, + ) = TvProgram( + id = UUID.randomUUID(), + channelId = channelId, + start = zeroHourStart.plusMinutes((startHours * 60).toLong()), + end = zeroHourStart.plusMinutes((endHours * 60).toLong()), + startHours = startHours, + endHours = endHours, + duration = ((endHours - startHours) * 60).toInt().minutes, + name = NO_DATA, + subtitle = null, + seasonEpisode = null, + isRecording = false, + isSeriesRecording = false, + isRepeat = false, + category = ProgramCategory.FAKE, + ) + } } enum class ProgramCategory( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt index 90ed0ee1..7db259d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt @@ -106,7 +106,7 @@ fun TvGuideGrid( channels = channels, programs = programs, channelProgramCount = viewModel.channelProgramCount, - start = viewModel.start, + start = viewModel.guideStart, onClickChannel = { index, channel -> viewModel.navigationManager.navigateTo( Destination.Playback(