feat: Implement stream link caching and reuse settings for improved playback continuity

This commit is contained in:
tapframe 2026-03-29 16:14:06 +05:30
parent ec2980d70e
commit 6ddae9a236
15 changed files with 447 additions and 7 deletions

View file

@ -13,6 +13,7 @@ import com.nuvio.app.features.player.PlayerSettingsStorage
import com.nuvio.app.features.profiles.ProfileStorage
import com.nuvio.app.features.settings.ThemeSettingsStorage
import com.nuvio.app.features.watched.WatchedStorage
import com.nuvio.app.features.streams.StreamLinkCacheStorage
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
import com.nuvio.app.features.watchprogress.WatchProgressStorage
@ -37,6 +38,7 @@ class MainActivity : ComponentActivity() {
ThemeSettingsStorage.initialize(applicationContext)
ContinueWatchingPreferencesStorage.initialize(applicationContext)
WatchProgressStorage.initialize(applicationContext)
StreamLinkCacheStorage.initialize(applicationContext)
setContent {
App()

View file

@ -11,6 +11,8 @@ actual object PlayerSettingsStorage {
private const val secondaryPreferredAudioLanguageKey = "secondary_preferred_audio_language"
private const val preferredSubtitleLanguageKey = "preferred_subtitle_language"
private const val secondaryPreferredSubtitleLanguageKey = "secondary_preferred_subtitle_language"
private const val streamReuseLastLinkEnabledKey = "stream_reuse_last_link_enabled"
private const val streamReuseLastLinkCacheHoursKey = "stream_reuse_last_link_cache_hours"
private var preferences: SharedPreferences? = null
@ -88,4 +90,38 @@ actual object PlayerSettingsStorage {
}
?.apply()
}
actual fun loadStreamReuseLastLinkEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(streamReuseLastLinkEnabledKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, false)
} else {
null
}
}
actual fun saveStreamReuseLastLinkEnabled(enabled: Boolean) {
preferences
?.edit()
?.putBoolean(ProfileScopedKey.of(streamReuseLastLinkEnabledKey), enabled)
?.apply()
}
actual fun loadStreamReuseLastLinkCacheHours(): Int? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getInt(key, 24)
} else {
null
}
}
actual fun saveStreamReuseLastLinkCacheHours(hours: Int) {
preferences
?.edit()
?.putInt(ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey), hours)
?.apply()
}
}

View file

@ -0,0 +1,3 @@
package com.nuvio.app.features.streams
internal actual fun epochMs(): Long = System.currentTimeMillis()

View file

@ -0,0 +1,32 @@
package com.nuvio.app.features.streams
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.storage.ProfileScopedKey
actual object StreamLinkCacheStorage {
private const val preferencesName = "nuvio_stream_link_cache"
private var preferences: SharedPreferences? = null
fun initialize(context: Context) {
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
}
actual fun loadEntry(hashedKey: String): String? =
preferences?.getString(ProfileScopedKey.of(hashedKey), null)
actual fun saveEntry(hashedKey: String, payload: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(hashedKey), payload)
?.apply()
}
actual fun removeEntry(hashedKey: String) {
preferences
?.edit()
?.remove(ProfileScopedKey.of(hashedKey))
?.apply()
}
}

View file

@ -85,8 +85,10 @@ import com.nuvio.app.features.settings.AccountSettingsScreen
import com.nuvio.app.features.settings.ThemeSettingsRepository
import com.nuvio.app.features.streams.StreamContext
import com.nuvio.app.features.streams.StreamContextStore
import com.nuvio.app.features.streams.StreamLinkCacheRepository
import com.nuvio.app.features.streams.StreamsRepository
import com.nuvio.app.features.streams.StreamsScreen
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.watched.WatchedRepository
import com.nuvio.app.features.watched.toWatchedItem
import com.nuvio.app.features.watched.watchedItemKey
@ -454,6 +456,52 @@ private fun MainAppContent(
StreamContextStore.get(contextId)?.pauseDescription
}
}
val playerSettings by remember {
PlayerSettingsRepository.ensureLoaded()
PlayerSettingsRepository.uiState
}.collectAsStateWithLifecycle()
// Reuse Last Link: auto-play from cache if enabled (only on first entry)
var reuseHandled by remember(route.videoId) { mutableStateOf(false) }
LaunchedEffect(route.videoId, playerSettings.streamReuseLastLinkEnabled) {
if (reuseHandled) return@LaunchedEffect
reuseHandled = true
if (!playerSettings.streamReuseLastLinkEnabled) return@LaunchedEffect
val cacheKey = StreamLinkCacheRepository.contentKey(route.type, route.videoId)
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
if (cached != null) {
val launchId = PlayerLaunchStore.put(
PlayerLaunch(
title = route.title,
sourceUrl = cached.url,
logo = route.logo,
poster = route.poster,
background = route.background,
seasonNumber = route.seasonNumber,
episodeNumber = route.episodeNumber,
episodeTitle = route.episodeTitle,
episodeThumbnail = route.episodeThumbnail,
streamTitle = cached.streamName,
streamSubtitle = null,
pauseDescription = pauseDescription,
providerName = cached.addonName,
providerAddonId = cached.addonId,
contentType = route.type,
videoId = route.videoId,
parentMetaId = route.parentMetaId ?: route.videoId,
parentMetaType = route.parentMetaType ?: route.type,
initialPositionMs = route.resumePositionMs ?: 0L,
)
)
route.streamContextId?.let(StreamContextStore::remove)
navController.navigate(PlayerRoute(launchId = launchId)) {
popUpTo<StreamRoute> { inclusive = true }
}
}
}
StreamsScreen(
type = route.type,
videoId = route.videoId,
@ -471,6 +519,19 @@ private fun MainAppContent(
onStreamSelected = { stream ->
val sourceUrl = stream.directPlaybackUrl
if (sourceUrl != null) {
// Persist for Reuse Last Link
if (playerSettings.streamReuseLastLinkEnabled) {
val cacheKey = StreamLinkCacheRepository.contentKey(route.type, route.videoId)
StreamLinkCacheRepository.save(
contentKey = cacheKey,
url = sourceUrl,
streamName = stream.streamLabel,
addonName = stream.addonName,
addonId = stream.addonId,
filename = stream.behaviorHints.filename,
videoSize = stream.behaviorHints.videoSize,
)
}
val launchId = PlayerLaunchStore.put(
PlayerLaunch(
title = route.title,

View file

@ -10,6 +10,8 @@ data class PlayerSettingsUiState(
val secondaryPreferredAudioLanguage: String? = null,
val preferredSubtitleLanguage: String = SubtitleLanguageOption.NONE,
val secondaryPreferredSubtitleLanguage: String? = null,
val streamReuseLastLinkEnabled: Boolean = false,
val streamReuseLastLinkCacheHours: Int = 24,
)
object PlayerSettingsRepository {
@ -22,6 +24,8 @@ object PlayerSettingsRepository {
private var secondaryPreferredAudioLanguage: String? = null
private var preferredSubtitleLanguage = SubtitleLanguageOption.NONE
private var secondaryPreferredSubtitleLanguage: String? = null
private var streamReuseLastLinkEnabled = false
private var streamReuseLastLinkCacheHours = 24
fun ensureLoaded() {
if (hasLoaded) return
@ -45,6 +49,8 @@ object PlayerSettingsRepository {
?: SubtitleLanguageOption.NONE
secondaryPreferredSubtitleLanguage =
normalizeLanguageCode(PlayerSettingsStorage.loadSecondaryPreferredSubtitleLanguage())
streamReuseLastLinkEnabled = PlayerSettingsStorage.loadStreamReuseLastLinkEnabled() ?: false
streamReuseLastLinkCacheHours = PlayerSettingsStorage.loadStreamReuseLastLinkCacheHours() ?: 24
publish()
}
@ -92,6 +98,22 @@ object PlayerSettingsRepository {
PlayerSettingsStorage.saveSecondaryPreferredSubtitleLanguage(normalized)
}
fun setStreamReuseLastLinkEnabled(enabled: Boolean) {
ensureLoaded()
if (streamReuseLastLinkEnabled == enabled) return
streamReuseLastLinkEnabled = enabled
publish()
PlayerSettingsStorage.saveStreamReuseLastLinkEnabled(enabled)
}
fun setStreamReuseLastLinkCacheHours(hours: Int) {
ensureLoaded()
if (streamReuseLastLinkCacheHours == hours) return
streamReuseLastLinkCacheHours = hours
publish()
PlayerSettingsStorage.saveStreamReuseLastLinkCacheHours(hours)
}
private fun publish() {
_uiState.value = PlayerSettingsUiState(
showLoadingOverlay = showLoadingOverlay,
@ -99,6 +121,8 @@ object PlayerSettingsRepository {
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
)
}
}

View file

@ -11,4 +11,8 @@ internal expect object PlayerSettingsStorage {
fun savePreferredSubtitleLanguage(language: String)
fun loadSecondaryPreferredSubtitleLanguage(): String?
fun saveSecondaryPreferredSubtitleLanguage(language: String?)
fun loadStreamReuseLastLinkEnabled(): Boolean?
fun saveStreamReuseLastLinkEnabled(enabled: Boolean)
fun loadStreamReuseLastLinkCacheHours(): Int?
fun saveStreamReuseLastLinkCacheHours(hours: Int)
}

View file

@ -47,6 +47,8 @@ internal fun LazyListScope.playbackSettingsContent(
secondaryPreferredAudioLanguage: String?,
preferredSubtitleLanguage: String,
secondaryPreferredSubtitleLanguage: String?,
streamReuseLastLinkEnabled: Boolean,
streamReuseLastLinkCacheHours: Int,
) {
item {
PlaybackSettingsSection(
@ -56,6 +58,8 @@ internal fun LazyListScope.playbackSettingsContent(
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
)
}
}
@ -68,11 +72,14 @@ private fun PlaybackSettingsSection(
secondaryPreferredAudioLanguage: String?,
preferredSubtitleLanguage: String,
secondaryPreferredSubtitleLanguage: String?,
streamReuseLastLinkEnabled: Boolean,
streamReuseLastLinkCacheHours: Int,
) {
var showPreferredAudioDialog by remember { mutableStateOf(false) }
var showSecondaryAudioDialog by remember { mutableStateOf(false) }
var showPreferredSubtitleDialog by remember { mutableStateOf(false) }
var showSecondarySubtitleDialog by remember { mutableStateOf(false) }
var showReuseCacheDurationDialog by remember { mutableStateOf(false) }
SettingsSection(
title = "PLAYER",
@ -130,6 +137,31 @@ private fun PlaybackSettingsSection(
}
}
SettingsSection(
title = "STREAM SELECTION",
isTablet = isTablet,
) {
SettingsGroup(isTablet = isTablet) {
SettingsSwitchRow(
title = "Reuse Last Link",
description = "Auto-play your last working stream for this same movie/episode when cache is still valid.",
checked = streamReuseLastLinkEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setStreamReuseLastLinkEnabled,
)
if (streamReuseLastLinkEnabled) {
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = "Last Link Cache Duration",
description = formatReuseCacheDuration(streamReuseLastLinkCacheHours),
icon = Icons.Rounded.Language,
isTablet = isTablet,
onClick = { showReuseCacheDurationDialog = true },
)
}
}
}
if (showPreferredAudioDialog) {
LanguageSelectionDialog(
title = "Preferred Audio Language",
@ -201,6 +233,26 @@ private fun PlaybackSettingsSection(
onDismiss = { showSecondarySubtitleDialog = false },
)
}
if (showReuseCacheDurationDialog) {
ReuseCacheDurationDialog(
selectedHours = streamReuseLastLinkCacheHours,
onDurationSelected = { hours ->
PlayerSettingsRepository.setStreamReuseLastLinkCacheHours(hours)
showReuseCacheDurationDialog = false
},
onDismiss = { showReuseCacheDurationDialog = false },
)
}
}
private fun formatReuseCacheDuration(hours: Int): String = when {
hours < 24 -> "$hours hour${if (hours != 1) "s" else ""}"
hours % 24 == 0 -> {
val days = hours / 24
"$days day${if (days != 1) "s" else ""}"
}
else -> "$hours hours"
}
private data class LanguageSelectionOption(
@ -297,3 +349,90 @@ private fun LanguageSelectionDialog(
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ReuseCacheDurationDialog(
selectedHours: Int,
onDurationSelected: (Int) -> Unit,
onDismiss: () -> Unit,
) {
val options = listOf(1, 6, 12, 24, 48, 72, 168)
BasicAlertDialog(
onDismissRequest = onDismiss,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Last Link Cache Duration",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
options.forEach { hours ->
val isSelected = hours == selectedHours
val containerColor = if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable { onDurationSelected(hours) },
shape = RoundedCornerShape(12.dp),
color = containerColor,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = formatReuseCacheDuration(hours),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center,
) {
if (isSelected) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
}
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "Tap outside to close",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}

View file

@ -81,6 +81,8 @@ fun SettingsScreen(
secondaryPreferredAudioLanguage = playerSettingsUiState.secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = playerSettingsUiState.preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = playerSettingsUiState.streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = playerSettingsUiState.streamReuseLastLinkCacheHours,
selectedTheme = selectedTheme,
onThemeSelected = ThemeSettingsRepository::setTheme,
amoledEnabled = amoledEnabled,
@ -100,6 +102,8 @@ fun SettingsScreen(
secondaryPreferredAudioLanguage = playerSettingsUiState.secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = playerSettingsUiState.preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = playerSettingsUiState.streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = playerSettingsUiState.streamReuseLastLinkCacheHours,
selectedTheme = selectedTheme,
onThemeSelected = ThemeSettingsRepository::setTheme,
amoledEnabled = amoledEnabled,
@ -123,6 +127,8 @@ private fun MobileSettingsScreen(
secondaryPreferredAudioLanguage: String?,
preferredSubtitleLanguage: String,
secondaryPreferredSubtitleLanguage: String?,
streamReuseLastLinkEnabled: Boolean,
streamReuseLastLinkCacheHours: Int,
selectedTheme: AppTheme,
onThemeSelected: (AppTheme) -> Unit,
amoledEnabled: Boolean,
@ -158,6 +164,8 @@ private fun MobileSettingsScreen(
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
)
SettingsPage.Appearance -> appearanceSettingsContent(
isTablet = false,
@ -185,6 +193,8 @@ private fun TabletSettingsScreen(
secondaryPreferredAudioLanguage: String?,
preferredSubtitleLanguage: String,
secondaryPreferredSubtitleLanguage: String?,
streamReuseLastLinkEnabled: Boolean,
streamReuseLastLinkCacheHours: Int,
selectedTheme: AppTheme,
onThemeSelected: (AppTheme) -> Unit,
amoledEnabled: Boolean,
@ -269,6 +279,8 @@ private fun TabletSettingsScreen(
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = secondaryPreferredSubtitleLanguage,
streamReuseLastLinkEnabled = streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = streamReuseLastLinkCacheHours,
)
SettingsPage.Appearance -> appearanceSettingsContent(
isTablet = true,

View file

@ -0,0 +1,72 @@
package com.nuvio.app.features.streams
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class CachedStreamLink(
val url: String,
val streamName: String,
val addonName: String,
val addonId: String,
val cachedAtMs: Long,
val filename: String? = null,
val videoSize: Long? = null,
)
internal expect fun epochMs(): Long
object StreamLinkCacheRepository {
private val json = Json { ignoreUnknownKeys = true }
fun contentKey(type: String, videoId: String): String =
"${type.lowercase()}|$videoId"
fun save(
contentKey: String,
url: String,
streamName: String,
addonName: String,
addonId: String,
filename: String? = null,
videoSize: Long? = null,
) {
val entry = CachedStreamLink(
url = url,
streamName = streamName,
addonName = addonName,
addonId = addonId,
cachedAtMs = epochMs(),
filename = filename,
videoSize = videoSize,
)
val payload = json.encodeToString(CachedStreamLink.serializer(), entry)
StreamLinkCacheStorage.saveEntry(hashedKey(contentKey), payload)
}
fun getValid(contentKey: String, maxAgeMs: Long): CachedStreamLink? {
if (maxAgeMs <= 0L) return null
val raw = StreamLinkCacheStorage.loadEntry(hashedKey(contentKey)) ?: return null
val entry = runCatching {
json.decodeFromString(CachedStreamLink.serializer(), raw)
}.getOrNull() ?: run {
StreamLinkCacheStorage.removeEntry(hashedKey(contentKey))
return null
}
val age = epochMs() - entry.cachedAtMs
if (entry.cachedAtMs <= 0L || age > maxAgeMs) {
StreamLinkCacheStorage.removeEntry(hashedKey(contentKey))
return null
}
if (entry.url.isBlank()) {
StreamLinkCacheStorage.removeEntry(hashedKey(contentKey))
return null
}
return entry
}
private fun hashedKey(contentKey: String): String {
val hash = contentKey.fold(0L) { acc, c -> acc * 31 + c.code }.toULong()
return "stream_link_$hash"
}
}

View file

@ -0,0 +1,7 @@
package com.nuvio.app.features.streams
internal expect object StreamLinkCacheStorage {
fun loadEntry(hashedKey: String): String?
fun saveEntry(hashedKey: String, payload: String)
fun removeEntry(hashedKey: String)
}

View file

@ -23,13 +23,7 @@ object StreamsRepository {
private var activeJob: Job? = null
private var activeRequestKey: String? = null
/**
* Loads streams for a given type + videoId from all installed addons that declare
* the "stream" resource for the given type (and matching idPrefixes if set).
*
* For movies: videoId == meta id (e.g. "tt1234567")
* For series: videoId == "{metaId}:{season}:{episode}" (e.g. "tt0898266:9:17")
*/
fun load(type: String, videoId: String) {
load(type = type, videoId = videoId, forceRefresh = false)
}

View file

@ -9,6 +9,8 @@ actual object PlayerSettingsStorage {
private const val secondaryPreferredAudioLanguageKey = "secondary_preferred_audio_language"
private const val preferredSubtitleLanguageKey = "preferred_subtitle_language"
private const val secondaryPreferredSubtitleLanguageKey = "secondary_preferred_subtitle_language"
private const val streamReuseLastLinkEnabledKey = "stream_reuse_last_link_enabled"
private const val streamReuseLastLinkCacheHoursKey = "stream_reuse_last_link_cache_hours"
actual fun loadShowLoadingOverlay(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
@ -75,4 +77,32 @@ actual object PlayerSettingsStorage {
defaults.setObject(language, forKey = key)
}
}
actual fun loadStreamReuseLastLinkEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(streamReuseLastLinkEnabledKey)
return if (defaults.objectForKey(key) != null) {
defaults.boolForKey(key)
} else {
null
}
}
actual fun saveStreamReuseLastLinkEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamReuseLastLinkEnabledKey))
}
actual fun loadStreamReuseLastLinkCacheHours(): Int? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey)
return if (defaults.objectForKey(key) != null) {
defaults.integerForKey(key).toInt()
} else {
null
}
}
actual fun saveStreamReuseLastLinkCacheHours(hours: Int) {
NSUserDefaults.standardUserDefaults.setInteger(hours.toLong(), forKey = ProfileScopedKey.of(streamReuseLastLinkCacheHoursKey))
}
}

View file

@ -0,0 +1,7 @@
package com.nuvio.app.features.streams
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
internal actual fun epochMs(): Long =
(NSDate().timeIntervalSince1970 * 1000.0).toLong()

View file

@ -0,0 +1,17 @@
package com.nuvio.app.features.streams
import com.nuvio.app.core.storage.ProfileScopedKey
import platform.Foundation.NSUserDefaults
actual object StreamLinkCacheStorage {
actual fun loadEntry(hashedKey: String): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(hashedKey))
actual fun saveEntry(hashedKey: String, payload: String) {
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(hashedKey))
}
actual fun removeEntry(hashedKey: String) {
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(hashedKey))
}
}