mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Fill missing guide data items
This commit is contained in:
parent
72b3fac44c
commit
88e3a2f77d
2 changed files with 183 additions and 61 deletions
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
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.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
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.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
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.GetProgramsDto
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
|
@ -40,7 +42,9 @@ import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.min
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
const val MAX_HOURS = 48L
|
const val MAX_HOURS = 48L
|
||||||
|
|
@ -55,7 +59,7 @@ class LiveTvViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
lateinit var start: LocalDateTime
|
lateinit var guideStart: LocalDateTime
|
||||||
private set
|
private set
|
||||||
private lateinit var channelsIdToIndex: Map<UUID, Int>
|
private lateinit var channelsIdToIndex: Map<UUID, Int>
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
@ -67,10 +71,10 @@ class LiveTvViewModel
|
||||||
val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val fetchedItem = MutableLiveData<BaseItem?>(null)
|
val fetchedItem = MutableLiveData<BaseItem?>(null)
|
||||||
|
|
||||||
private val range = 150
|
private val range = 100
|
||||||
|
|
||||||
fun init(firstLoad: Boolean) {
|
fun init(firstLoad: Boolean) {
|
||||||
start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
guideStart = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
||||||
if (!firstLoad) {
|
if (!firstLoad) {
|
||||||
loading.value = LoadingState.Loading
|
loading.value = LoadingState.Loading
|
||||||
}
|
}
|
||||||
|
|
@ -99,12 +103,19 @@ class LiveTvViewModel
|
||||||
Timber.d("Got ${channels.size} channels")
|
Timber.d("Got ${channels.size} channels")
|
||||||
channelsIdToIndex =
|
channelsIdToIndex =
|
||||||
channels.withIndex().associateBy({ it.value.id }, { it.index })
|
channels.withIndex().associateBy({ it.value.id }, { it.index })
|
||||||
fetchPrograms(channels, 0..<range.coerceAtMost(channels.size))
|
// Initially, quickly load the first 10 channels (only ~6 are visible immediately), then below will load more
|
||||||
|
// This makes the guide appear faster, and load more useable data in the background
|
||||||
|
val initial = 10
|
||||||
|
fetchPrograms(channels, 0..<initial.coerceAtMost(channels.size))
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LiveTvViewModel.channels.value = channels
|
this@LiveTvViewModel.channels.value = channels
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
|
// Now load the full range
|
||||||
|
if (channels.size > initial) {
|
||||||
|
fetchPrograms(channels, 0..<range.coerceAtMost(channels.size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,8 +132,8 @@ class LiveTvViewModel
|
||||||
channels: List<TvChannel>,
|
channels: List<TvChannel>,
|
||||||
range: IntRange,
|
range: IntRange,
|
||||||
) = mutex.withLock {
|
) = mutex.withLock {
|
||||||
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
|
val maxStartDate = guideStart.plusHours(MAX_HOURS).minusMinutes(1)
|
||||||
val minEndDate = start.plusMinutes(1L)
|
val minEndDate = guideStart.plusMinutes(1L)
|
||||||
val channelsToFetch = channels.subList(range.first, range.last + 1)
|
val channelsToFetch = channels.subList(range.first, range.last + 1)
|
||||||
Timber.v("Fetching programs for $range channels ${channelsToFetch.size}")
|
Timber.v("Fetching programs for $range channels ${channelsToFetch.size}")
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -132,11 +143,17 @@ class LiveTvViewModel
|
||||||
channelIds = channelsToFetch.map { it.id },
|
channelIds = channelsToFetch.map { it.id },
|
||||||
sortBy = listOf(ItemSortBy.START_DATE),
|
sortBy = listOf(ItemSortBy.START_DATE),
|
||||||
)
|
)
|
||||||
val programs =
|
val fetchedPrograms =
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
.getPrograms(request)
|
.getPrograms(request)
|
||||||
.content.items
|
.content.items
|
||||||
.map { dto ->
|
val programsByChannel = mutableMapOf<UUID, List<TvProgram>>()
|
||||||
|
val fetchedGroupedBy = fetchedPrograms.groupBy { it.channelId }
|
||||||
|
fetchedGroupedBy.forEach { (channelId, programs) ->
|
||||||
|
if (channelId != null) {
|
||||||
|
val listing = mutableListOf<TvProgram>()
|
||||||
|
programs as MutableList<BaseItemDto>
|
||||||
|
programs.forEachIndexed { index, dto ->
|
||||||
val category =
|
val category =
|
||||||
if (dto.isKids ?: false) {
|
if (dto.isKids ?: false) {
|
||||||
ProgramCategory.KIDS
|
ProgramCategory.KIDS
|
||||||
|
|
@ -149,13 +166,18 @@ class LiveTvViewModel
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
val p =
|
||||||
TvProgram(
|
TvProgram(
|
||||||
id = dto.id,
|
id = dto.id,
|
||||||
channelId = dto.channelId!!,
|
channelId = dto.channelId!!,
|
||||||
start = dto.startDate!!,
|
start = dto.startDate!!,
|
||||||
end = dto.endDate!!,
|
end = dto.endDate!!,
|
||||||
startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f),
|
startHours =
|
||||||
endHours = hoursBetween(start, dto.endDate!!),
|
hoursBetween(
|
||||||
|
guideStart,
|
||||||
|
dto.startDate!!,
|
||||||
|
).coerceAtLeast(0f),
|
||||||
|
endHours = hoursBetween(guideStart, dto.endDate!!),
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
duration = dto.runTimeTicks!!.ticks,
|
||||||
name = dto.seriesName ?: dto.name,
|
name = dto.seriesName ?: dto.name,
|
||||||
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
||||||
|
|
@ -170,22 +192,96 @@ class LiveTvViewModel
|
||||||
isRepeat = dto.isRepeat ?: false,
|
isRepeat = dto.isRepeat ?: false,
|
||||||
category = category,
|
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..<MAX_HOURS).forEach {
|
||||||
|
listing.add(
|
||||||
|
TvProgram.fake(
|
||||||
|
guideStart,
|
||||||
|
channelId,
|
||||||
|
it.toFloat(),
|
||||||
|
it + 1f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
programsByChannel[channelId] = listing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val programsByChannel = programs.groupBy { it.channelId }
|
|
||||||
val emptyChannels =
|
val emptyChannels =
|
||||||
channelsToFetch.filter { programsByChannel[it.id].orEmpty().isEmpty() }
|
channelsToFetch.filter { programsByChannel[it.id].orEmpty().isEmpty() }
|
||||||
val fake = mutableListOf<TvProgram>()
|
val fake = mutableListOf<TvProgram>()
|
||||||
val finalProgramsByChannel =
|
|
||||||
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(),
|
id = UUID.randomUUID(),
|
||||||
channelId = channel.id,
|
channelId = channel.id,
|
||||||
start = start.plusHours(it),
|
start = guideStart.plusHours(it),
|
||||||
end = start.plusHours(it + 1),
|
end = guideStart.plusHours(it + 1),
|
||||||
startHours = it.toFloat(),
|
startHours = it.toFloat(),
|
||||||
endHours = (it + 1).toFloat(),
|
endHours = (it + 1).toFloat(),
|
||||||
duration = 60.seconds,
|
duration = 60.seconds,
|
||||||
|
|
@ -198,25 +294,25 @@ class LiveTvViewModel
|
||||||
category = ProgramCategory.FAKE,
|
category = ProgramCategory.FAKE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put(channel.id, fakePrograms)
|
programsByChannel.put(channel.id, fakePrograms)
|
||||||
fake.addAll(fakePrograms)
|
fake.addAll(fakePrograms)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finalProgramsByChannel.forEach { (channelId, programs) ->
|
programsByChannel.forEach { (channelId, programs) ->
|
||||||
channelProgramCount[channelId] = programs.size
|
channelProgramCount[channelId] = programs.size
|
||||||
}
|
}
|
||||||
val finalProgramList =
|
val finalProgramList =
|
||||||
(programs + fake)
|
(programsByChannel.values.flatten())
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ channelsIdToIndex[it.channelId]!! },
|
{ channelsIdToIndex[it.channelId]!! },
|
||||||
{ it.start },
|
{ 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) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LiveTvViewModel.programs.value =
|
this@LiveTvViewModel.programs.value =
|
||||||
FetchedPrograms(range, finalProgramList, finalProgramsByChannel)
|
FetchedPrograms(range, finalProgramList, programsByChannel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -378,6 +474,32 @@ data class TvProgram(
|
||||||
val category: ProgramCategory?,
|
val category: ProgramCategory?,
|
||||||
) {
|
) {
|
||||||
val isFake = category == ProgramCategory.FAKE
|
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(
|
enum class ProgramCategory(
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ fun TvGuideGrid(
|
||||||
channels = channels,
|
channels = channels,
|
||||||
programs = programs,
|
programs = programs,
|
||||||
channelProgramCount = viewModel.channelProgramCount,
|
channelProgramCount = viewModel.channelProgramCount,
|
||||||
start = viewModel.start,
|
start = viewModel.guideStart,
|
||||||
onClickChannel = { index, channel ->
|
onClickChannel = { index, channel ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue