Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 48 additions & 53 deletions app/src/main/kotlin/com/arflix/tv/data/repository/IptvEpgIndex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,16 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(

fun replaceAll(sourceKey: String, nowNext: Map<String, IptvNowNext>, updatedAtMs: Long) {
if (sourceKey.isBlank() || nowNext.isEmpty()) return
val rows = flatten(nowNext)
if (rows.isEmpty()) return

writableDatabase.runInTransaction {
delete("epg_programs", "source_key = ?", arrayOf(sourceKey))
insertRows(sourceKey, rows)
insertNowNextRows(sourceKey, nowNext)
upsertSource(sourceKey, updatedAtMs)
}
}

fun replaceChannels(sourceKey: String, nowNext: Map<String, IptvNowNext>, updatedAtMs: Long) {
if (sourceKey.isBlank() || nowNext.isEmpty()) return
val rows = flatten(nowNext)
if (rows.isEmpty()) return

writableDatabase.runInTransaction {
nowNext.keys
Expand All @@ -79,7 +75,7 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(
val args = arrayOf(sourceKey) + channelIds.toTypedArray()
delete("epg_programs", "source_key = ? AND channel_id IN ($placeholders)", args)
}
insertRows(sourceKey, rows)
insertNowNextRows(sourceKey, nowNext)
upsertSource(sourceKey, updatedAtMs)
}
}
Expand Down Expand Up @@ -197,7 +193,21 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(
}
}

private fun SQLiteDatabase.insertRows(sourceKey: String, rows: List<ProgramRow>) {
private class ProgramDedupKey(val start: Long, val end: Long, val title: String) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ProgramDedupKey) return false
return start == other.start && end == other.end && title == other.title
}
override fun hashCode(): Int {
var result = start.hashCode()
result = 31 * result + end.hashCode()
result = 31 * result + title.hashCode()
return result
}
}

private fun SQLiteDatabase.insertNowNextRows(sourceKey: String, nowNext: Map<String, IptvNowNext>) {
val statement = compileStatement(
"""
INSERT OR REPLACE INTO epg_programs
Expand All @@ -206,20 +216,38 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(
""".trimIndent()
)
try {
rows.forEach { row ->
statement.clearBindings()
statement.bindString(1, sourceKey)
statement.bindString(2, row.channelId)
statement.bindLong(3, row.program.startUtcMillis)
statement.bindLong(4, row.program.endUtcMillis)
statement.bindString(5, row.program.title)
val description = row.program.description
if (description.isNullOrBlank()) {
statement.bindNull(6)
} else {
statement.bindString(6, description)
val seenPrograms = HashSet<ProgramDedupKey>(128)
nowNext.forEach { (channelId, item) ->
val normalizedId = channelId.trim()
if (normalizedId.isBlank()) return@forEach
seenPrograms.clear()

fun insertProgram(program: IptvProgram) {
if (program.title.isBlank() || program.endUtcMillis <= program.startUtcMillis) return
val titleTrimmed = program.title.trim()
val key = ProgramDedupKey(program.startUtcMillis, program.endUtcMillis, titleTrimmed)
if (!seenPrograms.add(key)) return

val description = program.description?.trim()?.take(MAX_DESCRIPTION_CHARS)
statement.clearBindings()
statement.bindString(1, sourceKey)
statement.bindString(2, normalizedId)
statement.bindLong(3, program.startUtcMillis)
statement.bindLong(4, program.endUtcMillis)
statement.bindString(5, titleTrimmed)
if (description.isNullOrBlank()) {
statement.bindNull(6)
} else {
statement.bindString(6, description)
}
statement.executeInsert()
}
statement.executeInsert()

item.now?.let(::insertProgram)
item.next?.let(::insertProgram)
item.later?.let(::insertProgram)
item.upcoming.forEach(::insertProgram)
item.recent.forEach(::insertProgram)
}
} finally {
statement.close()
Expand All @@ -236,35 +264,6 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(
}
}

private fun flatten(nowNext: Map<String, IptvNowNext>): List<ProgramRow> {
return buildList {
nowNext.forEach { (channelId, item) ->
val normalizedId = channelId.trim()
if (normalizedId.isBlank()) return@forEach
item.allPrograms()
.filter { it.title.isNotBlank() && it.endUtcMillis > it.startUtcMillis }
.distinctBy { "${it.startUtcMillis}|${it.endUtcMillis}|${it.title}" }
.forEach { program -> add(ProgramRow(normalizedId, program.compactForIndex())) }
}
}
}

private fun IptvNowNext.allPrograms(): List<IptvProgram> {
return buildList {
now?.let(::add)
next?.let(::add)
later?.let(::add)
addAll(upcoming)
addAll(recent)
}
}

private fun IptvProgram.compactForIndex(): IptvProgram =
copy(
title = title.trim(),
description = description?.trim()?.take(MAX_DESCRIPTION_CHARS)
)

private fun buildNowNext(programs: List<IptvProgram>, nowMs: Long): IptvNowNext? {
if (programs.isEmpty()) return null
val sorted = programs
Expand Down Expand Up @@ -315,10 +314,6 @@ internal class IptvEpgIndex(context: Context) : SQLiteOpenHelper(
}
}

private data class ProgramRow(
val channelId: String,
val program: IptvProgram
)

private companion object {
const val DATABASE_NAME = "arvio_iptv_epg_index.db"
Expand Down
60 changes: 44 additions & 16 deletions app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class IptvRepository @Inject constructor(

private val guideKeyCandidatesCache = java.util.Collections.synchronizedMap(
object : java.util.LinkedHashMap<String, Set<String>>(512, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<String, Set<String>>?): Boolean = size > 16384
override fun removeEldestEntry(eldest: Map.Entry<String, Set<String>>?): Boolean = size > 4096
}
)

Expand Down Expand Up @@ -1442,13 +1442,13 @@ class IptvRepository @Inject constructor(
)
}
if (playlistChannels.isNotEmpty()) {
aggregatedChannels += playlistChannels
val currentList = synchronized(aggregatedChannels) { aggregatedChannels.toList() }
aggregatedChannels.addAll(playlistChannels)
val currentList = synchronized(aggregatedChannels) { ArrayList(aggregatedChannels) }
runCatching { onChannelsReady(currentList) }
}
playlistChannels
}
}.awaitAll().flatten()
}.awaitAll()
synchronized(aggregatedChannels) { ArrayList(aggregatedChannels) }
}.also {
cachedChannels = it
cachedGroupedChannels = buildGroupedChannels(it)
Expand Down Expand Up @@ -1637,7 +1637,7 @@ class IptvRepository @Inject constructor(
if (resolved) {
shortEpgResult?.let { mergedXmlNowNext.putAll(it) } // Short EPG wins for channels it covers
resolvedNowNext = mergedXmlNowNext
cachedNowNext = ConcurrentHashMap(mergedXmlNowNext)
cachedNowNext = mergedXmlNowNext
cachedEpgAt = System.currentTimeMillis()
if (xmltvChanged) {
persistEpgIndexAll(config, mergedXmlNowNext, cachedEpgAt)
Expand Down Expand Up @@ -2447,6 +2447,8 @@ class IptvRepository @Inject constructor(

fun invalidateCache() {
cachedChannels = emptyList()
cachedChannelsLookupSource = null
cachedChannelsById = emptyMap()
cachedGroupedChannels = emptyMap()
cachedNowNext = ConcurrentHashMap()
cachedPlaylistAt = 0L
Expand Down Expand Up @@ -6235,7 +6237,20 @@ class IptvRepository @Inject constructor(
val country = extractFirstAttr(metadata, "tvg-country", "country")
val qualityLabel = extractFirstAttr(metadata, "quality", "tvg-quality", "resolution")
?: inferQualityLabel(channelName, groupTitle)
val requestHeaders = (pendingHeaders + extractInlineRequestHeaders(metadata)).filterValues { it.isNotBlank() }
val inlineHeaders = extractInlineRequestHeaders(metadata)
val requestHeaders = if (pendingHeaders.isEmpty()) {
if (inlineHeaders.isEmpty()) {
emptyMap()
} else {
inlineHeaders.filterValues { it.isNotBlank() }
}
} else {
if (inlineHeaders.isEmpty()) {
pendingHeaders.filterValues { it.isNotBlank() }
} else {
(pendingHeaders + inlineHeaders).filterValues { it.isNotBlank() }
}
}

channels += IptvChannel(
id = id,
Expand Down Expand Up @@ -6320,7 +6335,10 @@ class IptvRepository @Inject constructor(
if (!xmlId.isNullOrBlank()) {
val display = normalizeChannelKey(parser.nextText().orEmpty())
if (display.isNotBlank()) {
xmlChannelNameMap.getOrPut(xmlId) { mutableSetOf() }.add(display)
val isUseful = guideKeyCandidates(display).any { it in keyLookup }
if (isUseful) {
xmlChannelNameMap.getOrPut(xmlId) { mutableSetOf() }.add(display)
}
}
}
}
Expand Down Expand Up @@ -6490,7 +6508,10 @@ class IptvRepository @Inject constructor(
if (!xmlId.isNullOrBlank()) {
val display = normalizeChannelKey(textBuffer.toString())
if (display.isNotBlank()) {
xmlChannelNameMap.getOrPut(xmlId) { mutableSetOf() }.add(display)
val isUseful = guideKeyCandidates(display).any { it in keyLookup }
if (isUseful) {
xmlChannelNameMap.getOrPut(xmlId) { mutableSetOf() }.add(display)
}
}
}
readingDisplayName = false
Expand Down Expand Up @@ -6561,8 +6582,9 @@ class IptvRepository @Inject constructor(
nowCandidates: Map<String, IptvProgram?>,
upcomingCandidates: Map<String, List<IptvProgram>>,
recentCandidates: Map<String, List<IptvProgram>>
): Map<String, IptvNowNext> {
return channels.mapNotNull { channel ->
): ConcurrentHashMap<String, IptvNowNext> {
val result = ConcurrentHashMap<String, IptvNowNext>(channels.size)
channels.forEach { channel ->
val future = upcomingCandidates[channel.id].orEmpty()
val recent = recentCandidates[channel.id].orEmpty().sortedBy { it.startUtcMillis }
val nowNext = IptvNowNext(
Expand All @@ -6572,8 +6594,11 @@ class IptvRepository @Inject constructor(
upcoming = future,
recent = recent
)
if (hasProgramData(nowNext)) channel.id to nowNext else null
}.toMap()
if (hasProgramData(nowNext)) {
result[channel.id] = nowNext
}
}
return result
}

private fun pickNow(existing: IptvProgram?, candidate: IptvProgram, nowUtcMillis: Long): IptvProgram? {
Expand Down Expand Up @@ -6909,9 +6934,12 @@ class IptvRepository @Inject constructor(

private fun extractInlineRequestHeaders(metadata: String?): Map<String, String> {
if (metadata.isNullOrBlank()) return emptyMap()
val headers = linkedMapOf<String, String>()
extractFirstAttr(metadata, "http-user-agent", "user-agent")?.let { headers["User-Agent"] = it }
extractFirstAttr(metadata, "http-referrer", "http-referer", "referrer", "referer")?.let { headers["Referer"] = it }
val userAgent = extractFirstAttr(metadata, "http-user-agent", "user-agent")
val referrer = extractFirstAttr(metadata, "http-referrer", "http-referer", "referrer", "referer")
if (userAgent == null && referrer == null) return emptyMap()
val headers = LinkedHashMap<String, String>(2)
userAgent?.let { headers["User-Agent"] = it }
referrer?.let { headers["Referer"] = it }
return headers
}

Expand Down
Loading
Loading