From 0d1f92619facb5d5b84ff61020212c027721f161 Mon Sep 17 00:00:00 2001
From: Himanth-reddy <176995830+Himanth-reddy@users.noreply.github.com>
Date: Sun, 24 May 2026 13:47:50 +0000
Subject: [PATCH 1/5] chore(refactor): extract inline Regex instances to
constants for memory optimization
---
.../data/repository/CollectionTemplateManifest.kt | 5 ++++-
.../com/arflix/tv/ui/screens/player/PlayerScreen.kt | 13 +++++++++----
app/src/main/kotlin/com/arflix/tv/util/AppLogger.kt | 3 ++-
.../kotlin/com/arflix/tv/util/ProfileAvatarFiles.kt | 6 +++++-
.../kotlin/com/arflix/tv/util/SubtitleScoring.kt | 13 ++++++++-----
5 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CollectionTemplateManifest.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CollectionTemplateManifest.kt
index 3ba4e7cc..2814829d 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/CollectionTemplateManifest.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CollectionTemplateManifest.kt
@@ -1087,8 +1087,11 @@ internal object CollectionTemplateManifest {
.replace("+", "plus")
.replace("&", "and")
.replace("'", "")
- .replace(Regex("[^a-z0-9]+"), "_")
+ .replace(CollectionManifestRegexes.SLUGIFY_NON_ALPHA_NUM_REGEX, "_")
.trim('_')
}
}
+private object CollectionManifestRegexes {
+ val SLUGIFY_NON_ALPHA_NUM_REGEX = Regex("[^a-z0-9]+")
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
index 13425661..937e0e8d 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
@@ -3614,7 +3614,7 @@ private fun SubtitleMenu(
} else {
badge = subtitle.provider.ifBlank { null }
detail = subtitle.id
- .replace(Regex("^\\[[^]]+]"), "").trim()
+ .replace(PlayerScreenRegexes.BRACKET_REGEX, "").trim()
.ifBlank { subtitle.id }
.ifBlank { null }
}
@@ -4351,11 +4351,10 @@ private fun parseSizeToBytes(sizeStr: String): Long {
val normalized = sizeStr.uppercase()
.replace(",", ".")
- .replace(Regex("\\s+"), " ")
+ .replace(PlayerScreenRegexes.MULTI_SPACE_REGEX, " ")
.trim()
- val pattern = Regex("""(\d+(?:\.\d+)?)\s*(TB|GB|MB|KB)""")
- val match = pattern.find(normalized) ?: return 0L
+ val match = PlayerScreenRegexes.SIZE_REGEX.find(normalized) ?: return 0L
val number = match.groupValues[1].toDoubleOrNull() ?: return 0L
val multiplier = when (match.groupValues[2]) {
@@ -4624,3 +4623,9 @@ private fun subtitleMatchScore(streamSource: String, subtitle: Subtitle): Int {
if (subtitle.isEmbedded) return 100
return weightedSubtitleScore(streamSource, subtitle.id)
}
+
+private object PlayerScreenRegexes {
+ val BRACKET_REGEX = Regex("^\\[[^]]+]")
+ val MULTI_SPACE_REGEX = Regex("\\s+")
+ val SIZE_REGEX = Regex("""(\d+(?:\.\d+)?)\s*(TB|GB|MB|KB)""")
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/util/AppLogger.kt b/app/src/main/kotlin/com/arflix/tv/util/AppLogger.kt
index 77597587..983bc1e1 100644
--- a/app/src/main/kotlin/com/arflix/tv/util/AppLogger.kt
+++ b/app/src/main/kotlin/com/arflix/tv/util/AppLogger.kt
@@ -159,6 +159,7 @@ object AppLogger {
private val IPV4_PATTERN = Regex("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b")
private val TOKEN_PATTERN = Regex("(token|jwt|bearer|api[_-]?key|secret)[\"':\\s=]+([a-zA-Z0-9._-]{20,})", RegexOption.IGNORE_CASE)
private val LONG_HEX_PATTERN = Regex("[a-fA-F0-9]{32,}")
+ private val SAFE_TAG_PATTERN = Regex("[^A-Za-z0-9_.-]")
/**
* Sanitize a message by removing/masking PII.
@@ -202,7 +203,7 @@ object AppLogger {
private fun safeTag(tag: String): String {
return tag
- .replace(Regex("[^A-Za-z0-9_.-]"), "_")
+ .replace(SAFE_TAG_PATTERN, "_")
.take(40)
.ifBlank { "app" }
}
diff --git a/app/src/main/kotlin/com/arflix/tv/util/ProfileAvatarFiles.kt b/app/src/main/kotlin/com/arflix/tv/util/ProfileAvatarFiles.kt
index 9e15458e..a45b9723 100644
--- a/app/src/main/kotlin/com/arflix/tv/util/ProfileAvatarFiles.kt
+++ b/app/src/main/kotlin/com/arflix/tv/util/ProfileAvatarFiles.kt
@@ -26,5 +26,9 @@ object ProfileAvatarFiles {
}
private fun safeName(value: String): String =
- value.replace(Regex("[^A-Za-z0-9._-]"), "_")
+ value.replace(ProfileAvatarRegexes.SANITIZATION_REGEX, "_")
+}
+
+private object ProfileAvatarRegexes {
+ val SANITIZATION_REGEX = Regex("[^A-Za-z0-9._-]")
}
diff --git a/app/src/main/kotlin/com/arflix/tv/util/SubtitleScoring.kt b/app/src/main/kotlin/com/arflix/tv/util/SubtitleScoring.kt
index 3c23e7f2..bbc48fd6 100644
--- a/app/src/main/kotlin/com/arflix/tv/util/SubtitleScoring.kt
+++ b/app/src/main/kotlin/com/arflix/tv/util/SubtitleScoring.kt
@@ -49,8 +49,12 @@ private val FILE_EXT_RE = Regex("\\.(mkv|mp4|avi|mov|wmv|flv|m4v|ts|m2ts)$", Reg
// Merge "S06 E01" written as separate tokens back into "S06E01"
private val SPLIT_SEASON_RE = Regex("(s\\d{1,2})\\s+(e\\d{1,2})", RegexOption.IGNORE_CASE)
+private val PURE_NUMBERS_RE = Regex("\\d+")
+private val SUBTITLE_BRACKET_RE = Regex("^\\[[^]]+]")
+private val SEPARATOR_RE = Regex("[.\\-_\\s]+")
+
private fun tokenWeight(token: String): Int = when {
- token.matches(Regex("\\d+")) -> 0 // pure numbers: noise
+ token.matches(PURE_NUMBERS_RE) -> 0 // pure numbers: noise
EPISODE_RE.matches(token) -> 8 // S01E01
token in RESOLUTIONS -> 3
token in SOURCES -> 4
@@ -77,7 +81,7 @@ private fun tokenWeight(token: String): Int = when {
fun weightedSubtitleScore(streamSource: String, subtitleId: String): Int {
if (streamSource.isBlank() || subtitleId.isBlank()) return 0
- val cleanId = subtitleId.replace(Regex("^\\[[^]]+]"), "").trim()
+ val cleanId = subtitleId.replace(SUBTITLE_BRACKET_RE, "").trim()
// Normalise "S06 E01" → "S06E01" so space-separated season/episode merges with combined form
fun normalise(s: String) = SPLIT_SEASON_RE.replace(s) { it.groupValues[1] + it.groupValues[2] }
@@ -98,9 +102,8 @@ fun weightedSubtitleScore(streamSource: String, subtitleId: String): Int {
val (streamBody, streamGroup) = bodyAndGroup(normalise(streamSource))
val (subBody, subGroup) = bodyAndGroup(normalise(cleanId))
- val sep = Regex("[.\\-_\\s]+")
- val streamTokens = streamBody.lowercase().split(sep).filter { it.length > 1 }
- val subTokenSet = subBody.lowercase().split(sep).filter { it.length > 1 }.toSet()
+ val streamTokens = streamBody.lowercase().split(SEPARATOR_RE).filter { it.length > 1 }
+ val subTokenSet = subBody.lowercase().split(SEPARATOR_RE).filter { it.length > 1 }.toSet()
var totalWeight = 0
var matchedWeight = 0
From 39f23ecf8fec3cdcb68ac5f989b35660dfe31ba0 Mon Sep 17 00:00:00 2001
From: Himanth-reddy <176995830+Himanth-reddy@users.noreply.github.com>
Date: Tue, 26 May 2026 13:52:13 +0000
Subject: [PATCH 2/5] chore(refactor): optimize memory footprint by extracting
regex and formatting instances
---
.../repository/CatalogDiscoveryRepository.kt | 10 +++--
.../tv/data/repository/CatalogRepository.kt | 33 ++++++++-------
.../tv/data/repository/TraktRepository.kt | 18 ++++----
.../tv/ui/screens/details/DetailsViewModel.kt | 42 ++++++++++---------
.../tv/ui/screens/home/HomeViewModel.kt | 14 ++++---
.../ui/screens/settings/SettingsViewModel.kt | 3 +-
.../arflix/tv/ui/screens/tv/TvViewModel.kt | 6 ++-
7 files changed, 70 insertions(+), 56 deletions(-)
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogDiscoveryRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogDiscoveryRepository.kt
index 5746d09d..8d05d24a 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogDiscoveryRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogDiscoveryRepository.kt
@@ -187,7 +187,7 @@ class CatalogDiscoveryRepository @Inject constructor(
append(result.description.orEmpty().lowercase())
}
val tokens = query.lowercase()
- .split(NON_ALPHA_NUM_REGEX)
+ .split(CatalogDiscoveryRepoRegexes.NON_ALPHA_NUM_REGEX)
.filter { it.length >= 3 }
.distinct()
if (tokens.isEmpty()) return 0
@@ -197,7 +197,9 @@ class CatalogDiscoveryRepository @Inject constructor(
titleScore + bodyScore
}
}
- private companion object {
- private val NON_ALPHA_NUM_REGEX = Regex("[^a-z0-9]+")
- }
+
+}
+
+private object CatalogDiscoveryRepoRegexes {
+ val NON_ALPHA_NUM_REGEX = Regex("[^a-z0-9]+")
}
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogRepository.kt
index 250c8723..8e1beb32 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CatalogRepository.kt
@@ -973,9 +973,9 @@ class CatalogRepository @Inject constructor(
}
}
- val titleFromMeta = TITLE_FROM_META_REGEX.find(html)?.groupValues?.getOrNull(1)
+ val titleFromMeta = CatalogRepoRegexes.TITLE_FROM_META_REGEX.find(html)?.groupValues?.getOrNull(1)
- val titleFromTag = TITLE_FROM_TAG_REGEX.find(html)?.groupValues?.getOrNull(1)
+ val titleFromTag = CatalogRepoRegexes.TITLE_FROM_TAG_REGEX.find(html)?.groupValues?.getOrNull(1)
?.replace(" - MDBList", "", ignoreCase = true)
val titleFromSlug = extractMdblistSlugTitle(url)
@@ -999,7 +999,7 @@ class CatalogRepository @Inject constructor(
}
private fun extractTraktUrl(html: String): String? {
- return TRAKT_URL_REGEX.find(html)?.value
+ return CatalogRepoRegexes.TRAKT_URL_REGEX.find(html)?.value
}
private suspend fun fetchUrl(url: String): String? {
@@ -1291,18 +1291,6 @@ class CatalogRepository @Inject constructor(
private companion object {
private const val ADDON_SOURCE_REF_PREFIX = "addon_catalog|"
- private val TITLE_FROM_META_REGEX = Regex(
- """([^<]+)""",
- RegexOption.IGNORE_CASE
- )
- private val TRAKT_URL_REGEX = Regex(
- """https?://(?:www\.)?trakt\.tv/users/[^"'\s<]+/lists/[^"'\s<]+""",
- RegexOption.IGNORE_CASE
- )
}
}
@@ -1315,3 +1303,18 @@ private fun String.toDisplayTitle(): String {
}
.ifBlank { "Custom Catalog" }
}
+
+private object CatalogRepoRegexes {
+ val TITLE_FROM_META_REGEX = Regex(
+ """([^<]+)""",
+ RegexOption.IGNORE_CASE
+ )
+ val TRAKT_URL_REGEX = Regex(
+ """https?://(?:www\.)?trakt\.tv/users/[^"'\s<]+/lists/[^"'\s<]+""",
+ RegexOption.IGNORE_CASE
+ )
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
index 72662d33..edd5d9c2 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
@@ -2957,10 +2957,10 @@ class TraktRepository @Inject constructor(
private fun normalizeWatchlistTitle(title: String): String {
return Normalizer.normalize(title, Normalizer.Form.NFD)
- .replace(DIACRITICS_REGEX, "")
+ .replace(TraktRepoRegexes.DIACRITICS_REGEX, "")
.lowercase(Locale.US)
.replace("&", "and")
- .replace(NON_ALPHA_NUM_REGEX, " ")
+ .replace(TraktRepoRegexes.NON_ALPHA_NUM_REGEX, " ")
.trim()
.removePrefix("the ")
.removePrefix("a ")
@@ -3920,10 +3920,10 @@ private fun parseRuntimeLabelSeconds(label: String): Long {
if (normalized.isBlank()) return 0L
var minutes = 0L
- HOURS_REGEX.find(normalized)?.groupValues?.getOrNull(1)?.toLongOrNull()?.let { hours ->
+ TraktRepoRegexes.HOURS_REGEX.find(normalized)?.groupValues?.getOrNull(1)?.toLongOrNull()?.let { hours ->
minutes += hours * 60L
}
- MINS_REGEX.find(normalized)?.groupValues?.getOrNull(1)?.toLongOrNull()?.let { mins ->
+ TraktRepoRegexes.MINS_REGEX.find(normalized)?.groupValues?.getOrNull(1)?.toLongOrNull()?.let { mins ->
minutes += mins
}
@@ -3966,7 +3966,9 @@ private fun buildEpisodeKey(
}
-private val DIACRITICS_REGEX = Regex("\\p{Mn}+")
-private val NON_ALPHA_NUM_REGEX = Regex("[^a-z0-9]+")
-private val HOURS_REGEX = Regex("""(\d+)\s*h""")
-private val MINS_REGEX = Regex("""(\d+)\s*m""")
+private object TraktRepoRegexes {
+ val DIACRITICS_REGEX = Regex("\\p{Mn}+")
+ val NON_ALPHA_NUM_REGEX = Regex("[^a-z0-9]+")
+ val HOURS_REGEX = Regex("""(\d+)\s*h""")
+ val MINS_REGEX = Regex("""(\d+)\s*m""")
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsViewModel.kt
index 1437092c..68504bee 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsViewModel.kt
@@ -209,19 +209,6 @@ class DetailsViewModel @Inject constructor(
return value.isBlank() || value == "0.0" || value == "0"
}
- private val reviewWhitespaceRegex = Regex("\\s+")
- private val reviewMarkdownLinkRegex = Regex("\\[([^\\]]+)]\\([^)]*\\)")
- private val reviewHtmlTagRegex = Regex("<[^>]*>")
- private val reviewMarkdownNoiseRegex = Regex("[*_`>#]+")
- private val reviewSpamRegex = Regex(
- pattern = "\\b(?:https?://|www\\.|discord\\.gg|t\\.me/|telegram|whatsapp|onlyfans|casino|betting|viagra|loan|crypto|airdrop|promo\\s+code|coupon|download\\s+now|watch\\s+(?:free|online)|free\\s+stream|\\.xyz\\b|\\.top\\b|\\.click\\b|\\.link\\b|\\.site\\b)\\b",
- option = RegexOption.IGNORE_CASE
- )
- private val reviewDomainRegex = Regex(
- pattern = "\\b[a-z0-9-]+\\.(?:com|net|org|xyz|top|click|link|site|online|shop|info)\\b",
- option = RegexOption.IGNORE_CASE
- )
-
private fun normalizeAutoPlayMinQuality(raw: String?): String {
return when (raw?.trim()?.lowercase()) {
"any" -> "Any"
@@ -2307,7 +2294,7 @@ class DetailsViewModel @Inject constructor(
if (cleanedContent.length !in MIN_COMMUNITY_REVIEW_CHARS..MAX_COMMUNITY_REVIEW_CHARS) {
return@mapNotNull null
}
- val wordCount = cleanedContent.split(reviewWhitespaceRegex).count { it.length > 1 }
+ val wordCount = cleanedContent.split(DetailsVMRegexes.reviewWhitespaceRegex).count { it.length > 1 }
if (wordCount < MIN_COMMUNITY_REVIEW_WORDS) return@mapNotNull null
review.copy(content = cleanedContent)
}
@@ -2327,7 +2314,7 @@ class DetailsViewModel @Inject constructor(
if (cleanedContent.length !in MIN_COMMUNITY_REVIEW_CHARS..MAX_COMMUNITY_REVIEW_CHARS) {
return null
}
- val wordCount = cleanedContent.split(reviewWhitespaceRegex).count { it.length > 1 }
+ val wordCount = cleanedContent.split(DetailsVMRegexes.reviewWhitespaceRegex).count { it.length > 1 }
if (wordCount < MIN_COMMUNITY_REVIEW_WORDS) return null
val username = user?.username?.trim().orEmpty()
@@ -2350,17 +2337,17 @@ class DetailsViewModel @Inject constructor(
private fun cleanCommunityReviewText(raw: String): String {
return raw
- .replace(reviewMarkdownLinkRegex, "\$1")
- .replace(reviewHtmlTagRegex, " ")
- .replace(reviewMarkdownNoiseRegex, " ")
- .replace(reviewWhitespaceRegex, " ")
+ .replace(DetailsVMRegexes.reviewMarkdownLinkRegex, "\$1")
+ .replace(DetailsVMRegexes.reviewHtmlTagRegex, " ")
+ .replace(DetailsVMRegexes.reviewMarkdownNoiseRegex, " ")
+ .replace(DetailsVMRegexes.reviewWhitespaceRegex, " ")
.trim()
}
private fun isSpammyReviewText(raw: String): Boolean {
val text = raw.trim()
if (text.isBlank()) return true
- if (reviewSpamRegex.containsMatchIn(text) || reviewDomainRegex.containsMatchIn(text)) return true
+ if (DetailsVMRegexes.reviewSpamRegex.containsMatchIn(text) || DetailsVMRegexes.reviewDomainRegex.containsMatchIn(text)) return true
if (text.count { it == '$' } > 2 || text.count { it == '!' } > 6) return true
val visibleChars = text.count { !it.isWhitespace() }.coerceAtLeast(1)
@@ -2487,3 +2474,18 @@ class DetailsViewModel @Inject constructor(
prewarmVisibleStreams(mergedStreams)
}
}
+
+private object DetailsVMRegexes {
+ val reviewWhitespaceRegex = Regex("\\s+")
+ val reviewMarkdownLinkRegex = Regex("\\[([^\\]]+)]\\([^)]*\\)")
+ val reviewHtmlTagRegex = Regex("<[^>]*>")
+ val reviewMarkdownNoiseRegex = Regex("[*_`>#]+")
+ val reviewSpamRegex = Regex(
+ pattern = "\\b(?:https?://|www\\.|discord\\.gg|t\\.me/|telegram|whatsapp|onlyfans|casino|betting|viagra|loan|crypto|airdrop|promo\\s+code|coupon|download\\s+now|watch\\s+(?:free|online)|free\\s+stream|\\.xyz\\b|\\.top\\b|\\.click\\b|\\.link\\b|\\.site\\b)\\b",
+ option = RegexOption.IGNORE_CASE
+ )
+ val reviewDomainRegex = Regex(
+ pattern = "\\b[a-z0-9-]+\\.(?:com|net|org|xyz|top|click|link|site|online|shop|info)\\b",
+ option = RegexOption.IGNORE_CASE
+ )
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
index ae9a4ab4..7b98f14d 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
@@ -111,9 +111,6 @@ enum class ToastType {
SUCCESS, ERROR, INFO
}
-private val ALPHANUMERIC_REGEX = Regex("[^A-Za-z0-9_.-]")
-private val FILE_NAME_REGEX = Regex("[^a-zA-Z0-9._-]")
-
@HiltViewModel
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModel @Inject constructor(
@@ -741,9 +738,9 @@ class HomeViewModel @Inject constructor(
private fun categoriesCacheFile(): java.io.File {
val profileId = profileManager.getProfileIdSync()
.ifBlank { "default" }
- .replace(ALPHANUMERIC_REGEX, "_")
+ .replace(HomeVMRegexes.ALPHANUMERIC_REGEX, "_")
val language = (mediaRepository.contentLanguage ?: "en-US")
- .replace(ALPHANUMERIC_REGEX, "_")
+ .replace(HomeVMRegexes.ALPHANUMERIC_REGEX, "_")
return java.io.File(context.cacheDir, "home_categories_cache_${profileId}_$language.json")
}
@@ -4083,7 +4080,7 @@ class HomeViewModel @Inject constructor(
downloadJob = viewModelScope.launch {
updateStatusManager.updateStatus(com.arflix.tv.updater.UpdateStatus.Downloading(0f, update))
- val safeName = update.assetName.replace(FILE_NAME_REGEX, "_")
+ val safeName = update.assetName.replace(HomeVMRegexes.FILE_NAME_REGEX, "_")
val dest = java.io.File(java.io.File(context.cacheDir, "updates"), safeName)
val result = kotlinx.coroutines.withContext(Dispatchers.IO) {
@@ -4167,3 +4164,8 @@ class HomeViewModel @Inject constructor(
updateStatusManager.reset()
}
}
+
+private object HomeVMRegexes {
+ val ALPHANUMERIC_REGEX = Regex("[^A-Za-z0-9_.-]")
+ val FILE_NAME_REGEX = Regex("[^a-zA-Z0-9._-]")
+}
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt
index af04c1ad..31eae9eb 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt
@@ -229,7 +229,6 @@ class SettingsViewModel @Inject constructor(
}
}
-
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow = _uiState.asStateFlow()
@@ -441,7 +440,7 @@ class SettingsViewModel @Inject constructor(
} else {
gson.fromJson>(
json,
- object : TypeToken>() {}.type
+ TypeToken.getParameterized(List::class.java, QualityFilterConfig::class.java).type
).orEmpty()
}
}.getOrDefault(emptyList())
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
index ddcbc805..11081645 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
@@ -1017,7 +1017,7 @@ private fun buildPreparedChannelsByGroup(
private fun String.isNetherlandsGroup(): Boolean {
val tokens = lowercase()
- .split(Regex("[^a-z0-9]+"))
+ .split(TvVMRegexes.NON_ALPHA_NUM)
.filter { it.isNotBlank() }
.toSet()
return "netherlands" in tokens || "nederland" in tokens || "nl" in tokens
@@ -1087,3 +1087,7 @@ private fun IptvConfig.syncSignature(): String {
playlistsSignature
).joinToString("||")
}
+
+private object TvVMRegexes {
+ val NON_ALPHA_NUM = Regex("[^a-z0-9]+")
+}
From 9379dfd84c0c1da8a960ce629910bf57bf3bfda9 Mon Sep 17 00:00:00 2001
From: Himanth-reddy <176995830+Himanth-reddy@users.noreply.github.com>
Date: Sat, 30 May 2026 13:39:59 +0000
Subject: [PATCH 3/5] chore(refactor): optimize regex parsing and harden cloud
sync error handling
- Moved heavily used Regex constants out of functions into companion object in IptvRepository
- Refactored xmlAttribute parsing to use optimized string matching instead of dynamic regex in HomeServerRepository
- Modernized try-catch into runCatching in StreamRepository
- Wrapped volatile Cloud Sync Auth checks in runCatching to avoid flow crashes
---
.../data/repository/CloudSyncCoordinator.kt | 6 ++++--
.../data/repository/HomeServerRepository.kt | 21 +++++++++++++++++--
.../tv/data/repository/IptvRepository.kt | 11 +++++-----
3 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncCoordinator.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncCoordinator.kt
index 9c054c19..eb6c7cfe 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncCoordinator.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncCoordinator.kt
@@ -30,7 +30,8 @@ class CloudSyncCoordinator @Inject constructor(
if (!started.compareAndSet(false, true)) return
collectorJob = scope.launch {
invalidationBus.events.collectLatest { invalidation ->
- if (authRepository.getCurrentUserId().isNullOrBlank()) return@collectLatest
+ val userId = runCatching { authRepository.getCurrentUserId() }.getOrNull()
+ if (userId.isNullOrBlank()) return@collectLatest
cloudSyncRepository.markLocalStateDirtyNow()
scheduleFlush(invalidation)
}
@@ -54,7 +55,8 @@ class CloudSyncCoordinator @Inject constructor(
flushJob?.cancel()
flushJob = scope.launch {
delay(debounceMsFor(invalidation.scope))
- if (authRepository.getCurrentUserId().isNullOrBlank()) return@launch
+ val userId = runCatching { authRepository.getCurrentUserId() }.getOrNull()
+ if (userId.isNullOrBlank()) return@launch
runCatching { cloudSyncRepository.pushToCloud() }
.onFailure { error ->
Log.w("CloudSyncCoordinator", "Cloud push failed after ${invalidation.scope}: ${error.message}")
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
index 5e317d85..c1c888a4 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
@@ -2167,8 +2167,25 @@ class HomeServerRepository @Inject constructor(
runCatching { takeUnless { it.isJsonNull }?.asString }.getOrNull()
private fun String.xmlAttribute(name: String): String {
- val pattern = Regex("""\b${Regex.escape(name)}=["']([^"']*)["']""")
- return pattern.find(this)?.groupValues?.getOrNull(1).orEmpty().xmlDecoded()
+ val target = "$name="
+ var index = this.indexOf(target)
+ // Ensure word boundary check to avoid substring matches
+ while (index != -1) {
+ if (index == 0 || !(this[index - 1].isLetterOrDigit() || this[index - 1] == '_')) {
+ val valueStart = index + target.length
+ if (valueStart < this.length) {
+ val quote = this[valueStart]
+ if (quote == '"' || quote == '\'') {
+ val valueEnd = this.indexOf(quote, valueStart + 1)
+ if (valueEnd != -1) {
+ return this.substring(valueStart + 1, valueEnd).xmlDecoded()
+ }
+ }
+ }
+ }
+ index = this.indexOf(target, index + target.length)
+ }
+ return ""
}
private fun String.xmlBooleanAttribute(name: String): Boolean {
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
index 32890657..2f395b51 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
@@ -949,7 +949,7 @@ class IptvRepository @Inject constructor(
}
private fun String.replaceDurationScalePlaceholders(durationSec: Long): String {
- return Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""").replace(this) { match ->
+ return DURATION_SCALE_REGEX.replace(this) { match ->
val divisor = (match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
?: match.groupValues.getOrNull(2))
?.toLongOrNull()
@@ -1037,11 +1037,9 @@ class IptvRepository @Inject constructor(
}
private fun redactIptvUrl(url: String): String {
- val withoutQuerySecrets = Regex(
- pattern = """(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+"""
- ).replace(url) { match -> "${match.groupValues[1]}***" }
+ val withoutQuerySecrets = URL_QUERY_SECRETS_REGEX.replace(url) { match -> "${match.groupValues[1]}***" }
- return Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ return URL_PATH_SECRETS_REGEX
.replace(withoutQuerySecrets) { match ->
"${match.groupValues[1]}***/***${match.groupValues[4]}"
}
@@ -7143,6 +7141,9 @@ class IptvRepository @Inject constructor(
private companion object {
const val ENC_PREFIX = "encv1:"
const val ANDROID_KEYSTORE = "AndroidKeyStore"
+ val DURATION_SCALE_REGEX = Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""")
+ val URL_QUERY_SECRETS_REGEX = Regex("""(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+""")
+ val URL_PATH_SECRETS_REGEX = Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
const val CONFIG_KEY_ALIAS = "arvio_iptv_config_v1"
const val MAX_IPTV_CACHE_BYTES = 25L * 1024L * 1024L
const val IPTV_USER_AGENT = "VLC/3.0.20 LibVLC/3.0.20"
From 61f642df9b7c10c893a64fdb7240e346a34de6fd Mon Sep 17 00:00:00 2001
From: Himanth-reddy <176995830+Himanth-reddy@users.noreply.github.com>
Date: Mon, 1 Jun 2026 14:05:46 +0000
Subject: [PATCH 4/5] refactor(core): optimize stream parsing layers and harden
error handling
---
.../data/repository/HomeServerRepository.kt | 4 +-
.../tv/data/repository/IptvRepository.kt | 22 +++--
.../tv/data/repository/TraktRepository.kt | 91 +++++++++++++++++++
3 files changed, 108 insertions(+), 9 deletions(-)
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
index 5e317d85..61900fe5 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/HomeServerRepository.kt
@@ -2166,8 +2166,10 @@ class HomeServerRepository @Inject constructor(
private fun JsonElement.asStringOrNull(): String? =
runCatching { takeUnless { it.isJsonNull }?.asString }.getOrNull()
+ private val xmlAttributeRegexCache = java.util.concurrent.ConcurrentHashMap()
+
private fun String.xmlAttribute(name: String): String {
- val pattern = Regex("""\b${Regex.escape(name)}=["']([^"']*)["']""")
+ val pattern = xmlAttributeRegexCache.getOrPut(name) { Regex("""\b${Regex.escape(name)}=["']([^"']*)["']""") }
return pattern.find(this)?.groupValues?.getOrNull(1).orEmpty().xmlDecoded()
}
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
index 32890657..d9528fc9 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
@@ -949,7 +949,7 @@ class IptvRepository @Inject constructor(
}
private fun String.replaceDurationScalePlaceholders(durationSec: Long): String {
- return Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""").replace(this) { match ->
+ return DURATION_SCALE_REGEX.replace(this) { match ->
val divisor = (match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
?: match.groupValues.getOrNull(2))
?.toLongOrNull()
@@ -959,8 +959,10 @@ class IptvRepository @Inject constructor(
}
}
+ private val datePatternRegexCache = java.util.concurrent.ConcurrentHashMap()
+
private fun String.replaceDatePatternPlaceholders(key: String, dateTime: LocalDateTime): String {
- val regex = Regex("""\$\{""" + key + """:([^}]+)\}|\{""" + key + """:([^}]+)\}""")
+ val regex = datePatternRegexCache.getOrPut(key) { Regex("""\$\{""" + key + """:([^}]+)\}|\{""" + key + """:([^}]+)\}""") }
return regex.replace(this) { match ->
val pattern = match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
?: match.groupValues.getOrNull(2)
@@ -1037,11 +1039,9 @@ class IptvRepository @Inject constructor(
}
private fun redactIptvUrl(url: String): String {
- val withoutQuerySecrets = Regex(
- pattern = """(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+"""
- ).replace(url) { match -> "${match.groupValues[1]}***" }
+ val withoutQuerySecrets = QUERY_SECRETS_REGEX.replace(url) { match -> "${match.groupValues[1]}***" }
- return Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ return IPTV_CREDENTIALS_URL_REGEX
.replace(withoutQuerySecrets) { match ->
"${match.groupValues[1]}***/***${match.groupValues[4]}"
}
@@ -6174,8 +6174,8 @@ class IptvRepository @Inject constructor(
val base = epgId?.takeIf { it.isNotBlank() } ?: name
val normalizedBase = normalizeLooseKey(
base
- .replace(Regex("""\b(4K|UHD|FHD|HD|SD|2160P?|1080P?|720P?|576P?|480P?)\b""", RegexOption.IGNORE_CASE), " ")
- .replace(Regex("""\[[^\]]*]|\([^)]*\)"""), " ")
+ .replace(QUALITY_WORDS_REGEX, " ")
+ .replace(BRACKET_PAREN_REGEX, " ")
)
val normalizedGroup = normalizeLooseKey(group)
return listOf(normalizedGroup, normalizedBase).filter { it.isNotBlank() }.joinToString(":")
@@ -7141,6 +7141,12 @@ class IptvRepository @Inject constructor(
// ════════════════════════════════════════════════════════════════════════
private companion object {
+ private val DURATION_SCALE_REGEX = Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""")
+ private val QUERY_SECRETS_REGEX = Regex("""(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+""")
+ private val IPTV_CREDENTIALS_URL_REGEX = Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ private val QUALITY_WORDS_REGEX = Regex("""\b(4K|UHD|FHD|HD|SD|2160P?|1080P?|720P?|576P?|480P?)\b""", RegexOption.IGNORE_CASE)
+ private val BRACKET_PAREN_REGEX = Regex("""\[[^\]]*]|\([^)]*\)""")
+
const val ENC_PREFIX = "encv1:"
const val ANDROID_KEYSTORE = "AndroidKeyStore"
const val CONFIG_KEY_ALIAS = "arvio_iptv_config_v1"
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
index 72662d33..859a15ed 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/TraktRepository.kt
@@ -566,7 +566,14 @@ class TraktRepository @Inject constructor(
return try {
val watched = traktApi.getWatchedMovies(auth, clientId)
watched.mapNotNull { it.movie.ids.tmdb }.toSet()
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptySet()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptySet()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptySet()
}
}
@@ -591,7 +598,14 @@ class TraktRepository @Inject constructor(
}
}
episodes
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptySet()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptySet()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptySet()
}
}
@@ -3071,7 +3085,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getCollectionMovies(auth, clientId)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3083,7 +3104,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getCollectionShows(auth, clientId)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3177,7 +3205,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getRatingsMovies(auth, clientId)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3189,7 +3224,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getRatingsShows(auth, clientId)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3201,7 +3243,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getRatingsEpisodes(auth, clientId)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3313,7 +3362,14 @@ class TraktRepository @Inject constructor(
suspend fun getMovieComments(mediaId: String, page: Int = 1, limit: Int = 10, sort: String = "newest"): List {
return try {
traktApi.getMovieComments(clientId, "2", mediaId, sort, page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3328,7 +3384,14 @@ class TraktRepository @Inject constructor(
suspend fun getShowComments(mediaId: String, page: Int = 1, limit: Int = 10, sort: String = "newest"): List {
return try {
traktApi.getShowComments(clientId, "2", mediaId, sort, page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3343,7 +3406,14 @@ class TraktRepository @Inject constructor(
suspend fun getSeasonComments(showId: String, season: Int, page: Int = 1, limit: Int = 10, sort: String = "newest"): List {
return try {
traktApi.getSeasonComments(clientId, "2", showId, season, sort, page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3358,7 +3428,14 @@ class TraktRepository @Inject constructor(
suspend fun getEpisodeComments(showId: String, season: Int, episode: Int, page: Int = 1, limit: Int = 10, sort: String = "newest"): List {
return try {
traktApi.getEpisodeComments(clientId, "2", showId, season, episode, sort, page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3516,7 +3593,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getHistoryMovies(auth, clientId, "2", page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
@@ -3528,7 +3612,14 @@ class TraktRepository @Inject constructor(
val auth = getAuthHeader() ?: return emptyList()
return try {
traktApi.getHistoryEpisodes(auth, clientId, "2", page, limit)
+ } catch (e: java.io.IOException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Network or IO error, returning default", e)
+ emptyList()
+ } catch (e: retrofit2.HttpException) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "HTTP error fetching data, returning default", e)
+ emptyList()
} catch (e: Exception) {
+ com.arflix.tv.util.AppLogger.e("TraktRepository", "Unknown error fetching data, returning default", e)
emptyList()
}
}
From 758933babb22422f9615491bb21e9b2e2647a0b5 Mon Sep 17 00:00:00 2001
From: Himanth-reddy <176995830+Himanth-reddy@users.noreply.github.com>
Date: Tue, 2 Jun 2026 13:55:39 +0000
Subject: [PATCH 5/5] chore(refactor): optimize regex parsing and harden error
handling
---
.../tv/data/repository/IptvRepository.kt | 18 +++++++++++-------
.../tv/ui/screens/details/DetailsScreen.kt | 10 ++++++++--
.../arflix/tv/ui/screens/home/HomeViewModel.kt | 3 +++
.../player/AiSubtitleRenderersFactory.kt | 14 ++++++++++----
.../com/arflix/tv/ui/screens/tv/TvScreen.kt | 7 ++++++-
.../com/arflix/tv/ui/screens/tv/TvViewModel.kt | 7 ++++++-
.../tv/ui/screens/tv/live/LiveTvScreen.kt | 7 ++++++-
7 files changed, 50 insertions(+), 16 deletions(-)
diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
index 32890657..1c187470 100644
--- a/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
+++ b/app/src/main/kotlin/com/arflix/tv/data/repository/IptvRepository.kt
@@ -949,7 +949,7 @@ class IptvRepository @Inject constructor(
}
private fun String.replaceDurationScalePlaceholders(durationSec: Long): String {
- return Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""").replace(this) { match ->
+ return DURATION_PLACEHOLDER_REGEX.replace(this) { match ->
val divisor = (match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
?: match.groupValues.getOrNull(2))
?.toLongOrNull()
@@ -1037,11 +1037,9 @@ class IptvRepository @Inject constructor(
}
private fun redactIptvUrl(url: String): String {
- val withoutQuerySecrets = Regex(
- pattern = """(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+"""
- ).replace(url) { match -> "${match.groupValues[1]}***" }
+ val withoutQuerySecrets = IPTV_QUERY_SECRET_REGEX.replace(url) { match -> "${match.groupValues[1]}***" }
- return Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ return IPTV_URL_REDACT_REGEX
.replace(withoutQuerySecrets) { match ->
"${match.groupValues[1]}***/***${match.groupValues[4]}"
}
@@ -6174,8 +6172,8 @@ class IptvRepository @Inject constructor(
val base = epgId?.takeIf { it.isNotBlank() } ?: name
val normalizedBase = normalizeLooseKey(
base
- .replace(Regex("""\b(4K|UHD|FHD|HD|SD|2160P?|1080P?|720P?|576P?|480P?)\b""", RegexOption.IGNORE_CASE), " ")
- .replace(Regex("""\[[^\]]*]|\([^)]*\)"""), " ")
+ .replace(QUALITY_CLEANUP_REGEX, " ")
+ .replace(BRACKET_CLEANUP_REGEX, " ")
)
val normalizedGroup = normalizeLooseKey(group)
return listOf(normalizedGroup, normalizedBase).filter { it.isNotBlank() }.joinToString(":")
@@ -7141,6 +7139,12 @@ class IptvRepository @Inject constructor(
// ════════════════════════════════════════════════════════════════════════
private companion object {
+ val DURATION_PLACEHOLDER_REGEX = Regex("""\$\{duration:(\d+)\}|\{duration:(\d+)\}""")
+ val IPTV_URL_REDACT_REGEX = Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ val IPTV_QUERY_SECRET_REGEX = Regex("""(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+""")
+ val QUALITY_CLEANUP_REGEX = Regex("""\b(4K|UHD|FHD|HD|SD|2160P?|1080P?|720P?|576P?|480P?)\b""", RegexOption.IGNORE_CASE)
+ val BRACKET_CLEANUP_REGEX = Regex("""\[[^\]]*]|\([^)]*\)""")
+
const val ENC_PREFIX = "encv1:"
const val ANDROID_KEYSTORE = "AndroidKeyStore"
const val CONFIG_KEY_ALIAS = "arvio_iptv_config_v1"
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt
index e7b77b8c..c7a72685 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt
@@ -179,6 +179,12 @@ import java.util.Locale
import kotlin.math.abs
import androidx.compose.ui.res.stringResource
+
+private object DetailsScreenRegexes {
+ val FOUR_K_REGEX = Regex("""\b4[kK]\b""")
+ val YEAR_REGEX = Regex("""\d{4}""")
+}
+
/**
* Details screen for movies and TV shows
*/
@@ -1020,7 +1026,7 @@ private fun qualityScoreForAutoPlay(quality: String): Int {
private fun qualityScoreForStream(stream: com.arflix.tv.data.model.StreamSource): Int {
val combined = listOfNotNull(stream.quality, stream.source, stream.addonName).joinToString(" ")
return when {
- combined.contains("2160p", ignoreCase = true) || Regex("\\b4[kK]\\b").containsMatchIn(combined) -> 4
+ combined.contains("2160p", ignoreCase = true) || DetailsScreenRegexes.FOUR_K_REGEX.containsMatchIn(combined) -> 4
combined.contains("1080p", ignoreCase = true) -> 3
combined.contains("720p", ignoreCase = true) -> 2
combined.contains("480p", ignoreCase = true) -> 1
@@ -1200,7 +1206,7 @@ private fun DetailsContent(
}
val displayDate = item.year.takeIf { it.isNotBlank() }
?: item.releaseDate?.trim()?.takeIf { it.isNotEmpty() }?.let { date ->
- Regex("\\d{4}").find(date)?.value ?: date
+ DetailsScreenRegexes.YEAR_REGEX.find(date)?.value ?: date
}
?: ""
val hasDuration = item.duration.isNotEmpty() && item.duration != "0m"
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
index eaf4b6e4..b519d49d 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt
@@ -1494,6 +1494,7 @@ class HomeViewModel @Inject constructor(
try {
iptvRepository.warmXtreamVodCachesIfPossible()
} catch (e: Exception) {
+ AppLogger.e("HomeVM", "warmXtreamVodCachesIfPossible failed", e)
}
}
}
@@ -3084,6 +3085,7 @@ class HomeViewModel @Inject constructor(
}
} catch (e: Exception) {
// Silently fail - don't clear existing data on error
+ AppLogger.e("HomeVM", "launchContinueWatchingFetch failed", e)
}
}
}
@@ -3527,6 +3529,7 @@ class HomeViewModel @Inject constructor(
}
} catch (e: Exception) {
// Logo fetch failed
+ AppLogger.e("HomeVM", "Hero logo fetch failed", e)
}
}
}
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/AiSubtitleRenderersFactory.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/AiSubtitleRenderersFactory.kt
index 4c15dbda..c920c558 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/AiSubtitleRenderersFactory.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/AiSubtitleRenderersFactory.kt
@@ -11,6 +11,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+
+private object AiSubtitleRegexes {
+ val BRACKET_REGEX = Regex("""\[.*?\]""")
+ val MUSIC_REGEX = Regex("[♪♫]+")
+}
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class AiSubtitleRenderersFactory(
context: Context,
@@ -145,8 +151,8 @@ private class TranslatingTextOutput(
}
private fun stripHearingImpaired(text: String): String =
- text.replace(Regex("\\[.*?]"), "")
- .replace(Regex("[♪♫]+"), "")
+ text.replace(AiSubtitleRegexes.BRACKET_REGEX, "")
+ .replace(AiSubtitleRegexes.MUSIC_REGEX, "")
.trim()
private fun buildTranslated(group: CueGroup, originalCues: List, translatedText: String): CueGroup =
@@ -385,8 +391,8 @@ private class SubtitleOffsetRenderer(
.let { if (removeHI) stripHI(it) else it }
private fun stripHI(text: String): String =
- text.replace(Regex("\\[.*?]"), "")
- .replace(Regex("[♪♫]+"), "")
+ text.replace(AiSubtitleRegexes.BRACKET_REGEX, "")
+ .replace(AiSubtitleRegexes.MUSIC_REGEX, "")
.trim()
private fun findField(startClass: Class<*>, name: String): java.lang.reflect.Field? {
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt
index 54b53b44..92863e4f 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt
@@ -138,6 +138,11 @@ import java.time.format.DateTimeFormatter
import kotlinx.coroutines.delay
import kotlin.math.abs
+
+private object TvScreenRegexes {
+ val NON_ALPHANUMERIC_REGEX = Regex("""[^a-z0-9]+""")
+}
+
private enum class TvFocusZone {
SIDEBAR,
GROUPS,
@@ -169,7 +174,7 @@ private fun preferredStartupGroup(
private fun String.isPriorityGuideGroup(): Boolean {
if (this == FAVORITES_GROUP_NAME) return true
val tokens = lowercase()
- .split(Regex("[^a-z0-9]+"))
+ .split(TvScreenRegexes.NON_ALPHANUMERIC_REGEX)
.filter { it.isNotBlank() }
.toSet()
return "netherlands" in tokens || "nederland" in tokens || "nl" in tokens
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
index e654185f..28407917 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvViewModel.kt
@@ -24,6 +24,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
+
+private object TvViewModelRegexes {
+ val NON_ALPHANUMERIC_REGEX = Regex("""[^a-z0-9]+""")
+}
+
internal const val FAVORITES_GROUP_NAME = "My Favorites"
private const val EpgLoadingStateLimit = 800
private const val EpgAttemptedStateLimit = 2_400
@@ -1343,7 +1348,7 @@ private fun buildPreparedChannelsByGroup(
private fun String.isNetherlandsGroup(): Boolean {
val tokens = lowercase()
- .split(Regex("[^a-z0-9]+"))
+ .split(TvViewModelRegexes.NON_ALPHANUMERIC_REGEX)
.filter { it.isNotBlank() }
.toSet()
return "netherlands" in tokens || "nederland" in tokens || "nl" in tokens
diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvScreen.kt
index c87ec85b..4471e503 100644
--- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvScreen.kt
+++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/live/LiveTvScreen.kt
@@ -98,6 +98,11 @@ import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
+
+private object LiveTvScreenRegexes {
+ val IPTV_URL_REDACT_REGEX = Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+}
+
private enum class LiveTvFocusZone {
TOPBAR,
PROVIDER_SWITCHER,
@@ -1621,7 +1626,7 @@ private fun redactPlaybackUrl(url: String): String {
pattern = """(?i)([?&](?:username|user|uname|password|pass|pwd)=)[^&]+"""
).replace(url) { match -> "${match.groupValues[1]}***" }
- return Regex("""(?i)(/(?:live|movie|series|timeshift)/)([^/]+)/([^/]+)(/)""")
+ return LiveTvScreenRegexes.IPTV_URL_REDACT_REGEX
.replace(withoutQuerySecrets) { match ->
"${match.groupValues[1]}***/***${match.groupValues[4]}"
}