Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
69d797a
feat(iptv): make Stalker portal groups configurable like M3U playlists
openhands-agent Aug 23, 2026
cede927
ci: restore test-APK workflow (debug build, no secrets needed)
openhands-agent Aug 23, 2026
2b0b3df
ci: fix app-id suffix (no hyphens allowed in Android package names)
openhands-agent Aug 23, 2026
0858c22
feat(iptv): convert Stalker to a portal list (backend foundation)
openhands-agent Aug 24, 2026
b7197ad
fix(iptv): specify Triple/Pair type params for multi-portal merge
openhands-agent Aug 24, 2026
98cf2b0
ci: bump versionCode in test-apk builds so Android allows in-place up…
openhands-agent Aug 24, 2026
4e09c33
ci: add one-off workflow to generate a stable debug keystore
openhands-agent Aug 24, 2026
f680f90
ci: sign test APKs with a fixed debug keystore for in-place updates
openhands-agent Aug 24, 2026
1f8b7bd
fix(iptv): populate hidden groups and order in cache snapshots
openhands-agent Aug 24, 2026
dd17455
feat(iptv): multi-portal Stalker UI with CRUD, reorder and per-portal…
openhands-agent Aug 24, 2026
00e6510
fix(iptv): move STALKER_PLAYLIST_ID/MAX_STALKER_PORTALS to top-level …
openhands-agent Aug 24, 2026
c9649c4
fix(iptv): place Stalker dialogs in correct composable scope
openhands-agent Aug 24, 2026
2114198
fix(iptv): match mobile Stalker portal row to M3U style
openhands-agent Aug 24, 2026
3b3f726
fix(iptv): hide Stalker portal groups by portal id on touch UI
openhands-agent Aug 24, 2026
4e8270e
feat(settings): add leading icon and portal URL to TV Stalker rows
openhands-agent Aug 24, 2026
371c593
chore(ci): remove test-apk workflow and signing key for upstream
openhands-agent Aug 24, 2026
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
420 changes: 333 additions & 87 deletions app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
package com.arflix.tv.data.repository

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

/** Pseudo playlist id for the Stalker/Ministra portal source. */
const val STALKER_PLAYLIST_ID = "stalker"

/** Maximum number of Stalker portals a user can configure. */
const val MAX_STALKER_PORTALS = 3

/**
* Pure helpers for the Stalker multi-portal model. Kept dependency-free so they
* can be unit-tested without an Android context.
*/
internal object StalkerPortalSupport {

private val gson = Gson()

/**
* Channel ids use the `stalker:<portalId>:<origId>` shape. Returns the
* portal id segment, or null when the id is not a Stalker channel or the
* portal segment is missing.
*/
fun portalIdFromChannelId(channelId: String): String? {
if (!channelId.startsWith("stalker:")) return null
val afterPrefix = channelId.removePrefix("stalker:")
val portalId = afterPrefix.substringBefore(':', missingDelimiterValue = "")
return portalId.takeIf { it.isNotBlank() }
}

fun normalizeStalkerPortalEntry(
portal: StalkerPortalEntry,
index: Int
): StalkerPortalEntry? {
val portalUrl = runCatching { portal.portalUrl }.getOrNull().orEmpty().trim().trimEnd('/')
val macAddress = runCatching { portal.macAddress }.getOrNull().orEmpty().trim().uppercase()
if (portalUrl.isBlank() || macAddress.isBlank()) return null
return StalkerPortalEntry(
id = runCatching { portal.id }.getOrNull().orEmpty().trim().ifBlank { "stalker${index + 1}" },
name = runCatching { portal.name }.getOrNull().orEmpty().trim().ifBlank { "Portal ${index + 1}" },
portalUrl = portalUrl,
macAddress = macAddress,
enabled = runCatching { portal.enabled }.getOrDefault(true)
)
}

fun decodeStalkerPortals(raw: String, maxPortals: Int): List<StalkerPortalEntry> {
if (raw.isBlank()) return emptyList()
return runCatching {
val type = TypeToken.getParameterized(List::class.java, StalkerPortalEntry::class.java).type
gson.fromJson<List<StalkerPortalEntry>>(raw, type)
?.mapIndexed { index, portal -> normalizeStalkerPortalEntry(portal, index) }
?.filterNotNull()
?.take(maxPortals)
?: emptyList()
}.getOrDefault(emptyList())
}

/**
* Builds the migrated Portal 1 entry from legacy single-portal fields.
*/
fun migratedPortalFromLegacy(portalUrl: String, macAddress: String): StalkerPortalEntry? {
val url = portalUrl.trim().trimEnd('/')
val mac = macAddress.trim().uppercase()
if (url.isBlank() || mac.isBlank()) return null
return StalkerPortalEntry(
id = "stalker1",
name = "Portal 1",
portalUrl = url,
macAddress = mac
)
}
}

Large diffs are not rendered by default.

Loading
Loading