Batch-fetch IPTV now-playing titles with server-side EPG cache.
Add epg=1 to the channels API so the app loads program titles in one request per category instead of one call per channel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
da6159059d
commit
9377c9fa0e
6 changed files with 115 additions and 61 deletions
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 38
|
versionCode = 39
|
||||||
versionName = "0.11.6"
|
versionName = "0.11.7"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,12 @@ class ApiClient(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun iptvChannels(categoryId: String? = null): List<IptvChannel> = withContext(Dispatchers.IO) {
|
suspend fun iptvChannels(categoryId: String? = null, includeEpg: Boolean = false): List<IptvChannel> = withContext(Dispatchers.IO) {
|
||||||
val q = categoryId?.let { "?categoryId=${java.net.URLEncoder.encode(it, "UTF-8")}" }.orEmpty()
|
val params = buildList {
|
||||||
|
categoryId?.let { add("categoryId=${java.net.URLEncoder.encode(it, "UTF-8")}") }
|
||||||
|
if (includeEpg) add("epg=1")
|
||||||
|
}
|
||||||
|
val q = if (params.isEmpty()) "" else "?${params.joinToString("&")}"
|
||||||
val req = authed("$baseUrl/api/v1/client/iptv/channels$q").get().build()
|
val req = authed("$baseUrl/api/v1/client/iptv/channels$q").get().build()
|
||||||
parseObject(execute(req)) { o ->
|
parseObject(execute(req)) { o ->
|
||||||
val arr = o.optJSONArray("channels") ?: JSONArray()
|
val arr = o.optJSONArray("channels") ?: JSONArray()
|
||||||
|
|
@ -298,6 +302,7 @@ class ApiClient(
|
||||||
logoUrl = c.optString("logoUrl").ifBlank { null },
|
logoUrl = c.optString("logoUrl").ifBlank { null },
|
||||||
number = c.optInt("number").takeIf { c.has("number") && !c.isNull("number") },
|
number = c.optInt("number").takeIf { c.has("number") && !c.isNull("number") },
|
||||||
categoryId = c.optString("categoryId").ifBlank { null },
|
categoryId = c.optString("categoryId").ifBlank { null },
|
||||||
|
nowTitle = c.optString("nowTitle").ifBlank { null },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -481,6 +486,7 @@ data class IptvChannel(
|
||||||
val logoUrl: String? = null,
|
val logoUrl: String? = null,
|
||||||
val number: Int? = null,
|
val number: Int? = null,
|
||||||
val categoryId: String? = null,
|
val categoryId: String? = null,
|
||||||
|
val nowTitle: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class IptvPlayInfo(
|
data class IptvPlayInfo(
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,16 @@ import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
|
|
@ -69,7 +67,6 @@ import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import nl.vonas.mediacluster.tv.data.ApiClient
|
import nl.vonas.mediacluster.tv.data.ApiClient
|
||||||
import nl.vonas.mediacluster.tv.data.IptvCategory
|
import nl.vonas.mediacluster.tv.data.IptvCategory
|
||||||
import nl.vonas.mediacluster.tv.data.IptvChannel
|
import nl.vonas.mediacluster.tv.data.IptvChannel
|
||||||
|
|
@ -92,7 +89,6 @@ fun LiveTvPlayerScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val player = remember { buildLiveTvPlayer(context) }
|
val player = remember { buildLiveTvPlayer(context) }
|
||||||
val playerFocus = remember { FocusRequester() }
|
val playerFocus = remember { FocusRequester() }
|
||||||
val menuScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
var currentStreamId by remember { mutableIntStateOf(streamId) }
|
var currentStreamId by remember { mutableIntStateOf(streamId) }
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
|
|
@ -112,8 +108,8 @@ fun LiveTvPlayerScreen(
|
||||||
var menuCategoryId by remember { mutableStateOf(categoryId) }
|
var menuCategoryId by remember { mutableStateOf(categoryId) }
|
||||||
var menuCategoryName by remember { mutableStateOf("Zenders") }
|
var menuCategoryName by remember { mutableStateOf("Zenders") }
|
||||||
var menuRefreshing by remember { mutableStateOf(false) }
|
var menuRefreshing by remember { mutableStateOf(false) }
|
||||||
var epgFetchGen by remember { mutableIntStateOf(0) }
|
|
||||||
var lastCategoryShiftAt by remember { mutableLongStateOf(0L) }
|
var lastCategoryShiftAt by remember { mutableLongStateOf(0L) }
|
||||||
|
var menuLoadGen by remember { mutableIntStateOf(0) }
|
||||||
val menuEpgNow = remember { mutableStateMapOf<Int, String>() }
|
val menuEpgNow = remember { mutableStateMapOf<Int, String>() }
|
||||||
val menuListState = rememberLazyListState()
|
val menuListState = rememberLazyListState()
|
||||||
val menuChannelFocus = remember { mutableStateMapOf<Int, FocusRequester>() }
|
val menuChannelFocus = remember { mutableStateMapOf<Int, FocusRequester>() }
|
||||||
|
|
@ -180,25 +176,6 @@ fun LiveTvPlayerScreen(
|
||||||
runCatching { epg = api.iptvEpg(currentStreamId) }
|
runCatching { epg = api.iptvEpg(currentStreamId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchEpgTitles(ids: List<Int>, gen: Int) {
|
|
||||||
for (id in ids) {
|
|
||||||
if (gen != epgFetchGen) return
|
|
||||||
if (menuEpgNow.containsKey(id)) continue
|
|
||||||
runCatching {
|
|
||||||
api.iptvEpg(id).now?.title?.let { title ->
|
|
||||||
if (title.isNotBlank()) menuEpgNow[id] = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prefetchMenuEpg(ids: List<Int>) {
|
|
||||||
val gen = epgFetchGen
|
|
||||||
menuScope.launch {
|
|
||||||
fetchEpgTitles(ids, gen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(channelMenuOpen) {
|
LaunchedEffect(channelMenuOpen) {
|
||||||
if (!channelMenuOpen) return@LaunchedEffect
|
if (!channelMenuOpen) return@LaunchedEffect
|
||||||
if (menuCategories.isEmpty()) {
|
if (menuCategories.isEmpty()) {
|
||||||
|
|
@ -206,26 +183,32 @@ fun LiveTvPlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun applyMenuEpg(channels: List<IptvChannel>) {
|
||||||
|
for (ch in channels) {
|
||||||
|
ch.nowTitle?.let { menuEpgNow[ch.streamId] = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(channelMenuOpen, menuCategoryId) {
|
LaunchedEffect(channelMenuOpen, menuCategoryId) {
|
||||||
if (!channelMenuOpen) return@LaunchedEffect
|
if (!channelMenuOpen) return@LaunchedEffect
|
||||||
epgFetchGen++
|
menuLoadGen++
|
||||||
val gen = epgFetchGen
|
val gen = menuLoadGen
|
||||||
menuRefreshing = true
|
menuRefreshing = true
|
||||||
try {
|
try {
|
||||||
val catId = menuCategoryId
|
val catId = menuCategoryId
|
||||||
menuCategoryName = menuCategories.firstOrNull { it.id == catId }?.name ?: "Zenders"
|
menuCategoryName = menuCategories.firstOrNull { it.id == catId }?.name ?: "Zenders"
|
||||||
val loaded = if (!catId.isNullOrBlank()) {
|
val loaded = if (!catId.isNullOrBlank()) {
|
||||||
api.iptvChannels(catId)
|
api.iptvChannels(catId, includeEpg = true)
|
||||||
} else {
|
} else {
|
||||||
api.iptvChannels()
|
api.iptvChannels(includeEpg = true)
|
||||||
}
|
}
|
||||||
if (gen != epgFetchGen) return@LaunchedEffect
|
if (gen != menuLoadGen) return@LaunchedEffect
|
||||||
menuChannels = loaded
|
menuChannels = loaded
|
||||||
prefetchMenuEpg(loaded.take(18).map { it.streamId })
|
applyMenuEpg(loaded)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
if (gen == epgFetchGen) menuChannels = emptyList()
|
if (gen == menuLoadGen) menuChannels = emptyList()
|
||||||
} finally {
|
} finally {
|
||||||
if (gen == epgFetchGen) menuRefreshing = false
|
if (gen == menuLoadGen) menuRefreshing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,12 +224,6 @@ fun LiveTvPlayerScreen(
|
||||||
onCategoryChange(next.id)
|
onCategoryChange(next.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMenuChannelFocused(index: Int) {
|
|
||||||
val start = (index - 2).coerceAtLeast(0)
|
|
||||||
val ids = menuChannels.drop(start).take(6).map { it.streamId }
|
|
||||||
prefetchMenuEpg(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(channelMenuOpen, menuCategoryId, menuChannels) {
|
LaunchedEffect(channelMenuOpen, menuCategoryId, menuChannels) {
|
||||||
if (!channelMenuOpen || menuChannels.isEmpty()) return@LaunchedEffect
|
if (!channelMenuOpen || menuChannels.isEmpty()) return@LaunchedEffect
|
||||||
delay(80)
|
delay(80)
|
||||||
|
|
@ -459,7 +436,6 @@ fun LiveTvPlayerScreen(
|
||||||
channelFocus = menuChannelFocus,
|
channelFocus = menuChannelFocus,
|
||||||
onSelect = { selectChannel(it) },
|
onSelect = { selectChannel(it) },
|
||||||
onCategoryShift = { shiftMenuCategory(it) },
|
onCategoryShift = { shiftMenuCategory(it) },
|
||||||
onChannelFocused = { onMenuChannelFocused(it) },
|
|
||||||
modifier = Modifier.align(Alignment.CenterEnd),
|
modifier = Modifier.align(Alignment.CenterEnd),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -503,7 +479,6 @@ private fun LiveChannelSwitcher(
|
||||||
channelFocus: MutableMap<Int, FocusRequester>,
|
channelFocus: MutableMap<Int, FocusRequester>,
|
||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
onCategoryShift: (Int) -> Unit,
|
onCategoryShift: (Int) -> Unit,
|
||||||
onChannelFocused: (Int) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val glassShape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp)
|
val glassShape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp)
|
||||||
|
|
@ -618,10 +593,7 @@ private fun LiveChannelSwitcher(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 10.dp)
|
.padding(horizontal = 10.dp)
|
||||||
.focusRequester(fr)
|
.focusRequester(fr),
|
||||||
.onFocusChanged { state ->
|
|
||||||
if (state.isFocused) onChannelFocused(index)
|
|
||||||
},
|
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(8.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(8.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,10 @@ fun TvScreen(
|
||||||
focusedEpg = null
|
focusedEpg = null
|
||||||
focusedStreamId = null
|
focusedStreamId = null
|
||||||
try {
|
try {
|
||||||
channels = api.iptvChannels(catId)
|
channels = api.iptvChannels(catId, includeEpg = true)
|
||||||
|
for (ch in channels) {
|
||||||
|
ch.nowTitle?.let { epgNowTitles[ch.streamId] = it }
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e.message
|
error = e.message
|
||||||
channels = emptyList()
|
channels = emptyList()
|
||||||
|
|
@ -247,7 +250,7 @@ fun TvScreen(
|
||||||
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
||||||
ChannelRow(
|
ChannelRow(
|
||||||
channel = ch,
|
channel = ch,
|
||||||
nowTitle = epgNowTitles[ch.streamId],
|
nowTitle = ch.nowTitle ?: epgNowTitles[ch.streamId],
|
||||||
isFocused = focusedStreamId == ch.streamId,
|
isFocused = focusedStreamId == ch.streamId,
|
||||||
isLastWatched = ch.streamId == lastStreamId,
|
isLastWatched = ch.streamId == lastStreamId,
|
||||||
modifier = Modifier.focusRequester(fr),
|
modifier = Modifier.focusRequester(fr),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { loadXtreamForToken } from "../iptv/service";
|
||||||
import {
|
import {
|
||||||
decodeEpgText,
|
decodeEpgText,
|
||||||
epgToIso,
|
epgToIso,
|
||||||
|
type XtreamClient,
|
||||||
|
type XtreamEpgListing,
|
||||||
type XtreamLiveStream,
|
type XtreamLiveStream,
|
||||||
} from "../iptv/xtream";
|
} from "../iptv/xtream";
|
||||||
|
|
||||||
|
|
@ -106,7 +108,8 @@ function sortChannels(streams: XtreamLiveStream[]): XtreamLiveStream[] {
|
||||||
export async function getViewerIptvChannels(
|
export async function getViewerIptvChannels(
|
||||||
viewerId: string,
|
viewerId: string,
|
||||||
config: Config,
|
config: Config,
|
||||||
categoryId?: string
|
categoryId?: string,
|
||||||
|
includeEpg = false
|
||||||
) {
|
) {
|
||||||
const tokenId = await resolveViewerIptvToken(viewerId);
|
const tokenId = await resolveViewerIptvToken(viewerId);
|
||||||
if (!tokenId) throw new AppError("FORBIDDEN", "Geen TV-toegang", 403);
|
if (!tokenId) throw new AppError("FORBIDDEN", "Geen TV-toegang", 403);
|
||||||
|
|
@ -120,6 +123,10 @@ export async function getViewerIptvChannels(
|
||||||
: await xt.client.getLiveStreams(categoryId);
|
: await xt.client.getLiveStreams(categoryId);
|
||||||
streams = sortChannels(streams);
|
streams = sortChannels(streams);
|
||||||
|
|
||||||
|
const nowTitles = includeEpg
|
||||||
|
? await getCachedCategoryNowTitles(tokenId, categoryId, streams, xt.client)
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channels: streams.map((s) => ({
|
channels: streams.map((s) => ({
|
||||||
streamId: s.stream_id,
|
streamId: s.stream_id,
|
||||||
|
|
@ -127,6 +134,7 @@ export async function getViewerIptvChannels(
|
||||||
logoUrl: s.stream_icon || null,
|
logoUrl: s.stream_icon || null,
|
||||||
number: s.num ?? null,
|
number: s.num ?? null,
|
||||||
categoryId: s.category_id ? String(s.category_id) : null,
|
categoryId: s.category_id ? String(s.category_id) : null,
|
||||||
|
nowTitle: nowTitles?.get(s.stream_id) ?? null,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -188,6 +196,28 @@ export async function getViewerIptvEpg(viewerId: string, config: Config, streamI
|
||||||
if (!ch) throw new AppError("NOT_FOUND", "Zender niet gevonden", 404);
|
if (!ch) throw new AppError("NOT_FOUND", "Zender niet gevonden", 404);
|
||||||
|
|
||||||
const epg = await xt.client.getShortEpg(streamId, 8);
|
const epg = await xt.client.getShortEpg(streamId, 8);
|
||||||
|
const { now: nowProgram, next: nextProgram } = parseNowNextPrograms(epg);
|
||||||
|
|
||||||
|
return {
|
||||||
|
streamId,
|
||||||
|
channelName: ch.name,
|
||||||
|
logoUrl: ch.stream_icon || null,
|
||||||
|
now: nowProgram,
|
||||||
|
next: nextProgram,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EPG_NOW_CACHE_TTL_MS = 90_000;
|
||||||
|
const epgNowCache = new Map<string, { at: number; titles: Map<number, string> }>();
|
||||||
|
|
||||||
|
function epgCacheKey(tokenId: string, categoryId?: string): string {
|
||||||
|
return `${tokenId}:${categoryId?.trim() || "__all__"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNowNextPrograms(epg: XtreamEpgListing[]): {
|
||||||
|
now: { title: string; start: string; end: string | null } | null;
|
||||||
|
next: { title: string; start: string; end: string | null } | null;
|
||||||
|
} {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let nowProgram: { title: string; start: string; end: string | null } | null = null;
|
let nowProgram: { title: string; start: string; end: string | null } | null = null;
|
||||||
let nextProgram: { title: string; start: string; end: string | null } | null = null;
|
let nextProgram: { title: string; start: string; end: string | null } | null = null;
|
||||||
|
|
@ -207,11 +237,53 @@ export async function getViewerIptvEpg(viewerId: string, config: Config, streamI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { now: nowProgram, next: nextProgram };
|
||||||
streamId,
|
}
|
||||||
channelName: ch.name,
|
|
||||||
logoUrl: ch.stream_icon || null,
|
async function getCachedCategoryNowTitles(
|
||||||
now: nowProgram,
|
tokenId: string,
|
||||||
next: nextProgram,
|
categoryId: string | undefined,
|
||||||
};
|
streams: XtreamLiveStream[],
|
||||||
|
client: XtreamClient
|
||||||
|
): Promise<Map<number, string>> {
|
||||||
|
const key = epgCacheKey(tokenId, categoryId);
|
||||||
|
const cached = epgNowCache.get(key);
|
||||||
|
if (cached && Date.now() - cached.at < EPG_NOW_CACHE_TTL_MS) {
|
||||||
|
return cached.titles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = await fetchNowTitlesParallel(
|
||||||
|
client,
|
||||||
|
streams.map((s) => s.stream_id),
|
||||||
|
14
|
||||||
|
);
|
||||||
|
epgNowCache.set(key, { at: Date.now(), titles });
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNowTitlesParallel(
|
||||||
|
client: XtreamClient,
|
||||||
|
streamIds: number[],
|
||||||
|
concurrency: number
|
||||||
|
): Promise<Map<number, string>> {
|
||||||
|
const result = new Map<number, string>();
|
||||||
|
if (streamIds.length === 0) return result;
|
||||||
|
|
||||||
|
const queue = [...streamIds];
|
||||||
|
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const streamId = queue.shift();
|
||||||
|
if (streamId == null) return;
|
||||||
|
try {
|
||||||
|
const epg = await client.getShortEpg(streamId, 4);
|
||||||
|
const { now } = parseNowNextPrograms(epg);
|
||||||
|
if (now?.title) result.set(streamId, now.title);
|
||||||
|
} catch {
|
||||||
|
// zender zonder EPG overslaan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(workers);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,9 +240,10 @@ export function registerViewerRoutes(
|
||||||
|
|
||||||
app.get("/api/v1/client/iptv/channels", async (request) => {
|
app.get("/api/v1/client/iptv/channels", async (request) => {
|
||||||
const auth = await viewers.authFromBearer(request.headers.authorization);
|
const auth = await viewers.authFromBearer(request.headers.authorization);
|
||||||
const q = request.query as { categoryId?: string };
|
const q = request.query as { categoryId?: string; epg?: string };
|
||||||
const { getViewerIptvChannels } = await import("./iptv-client");
|
const { getViewerIptvChannels } = await import("./iptv-client");
|
||||||
return getViewerIptvChannels(auth.viewerId, config, q.categoryId);
|
const includeEpg = q.epg === "1" || q.epg === "true";
|
||||||
|
return getViewerIptvChannels(auth.viewerId, config, q.categoryId, includeEpg);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/v1/client/iptv/channels/:streamId/play", async (request) => {
|
app.get("/api/v1/client/iptv/channels/:streamId/play", async (request) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue