feat(debrid): sorting and filtering

This commit is contained in:
tapframe 2026-05-19 02:21:25 +05:30
parent 5aee64e25e
commit 3e40e47b78
10 changed files with 2232 additions and 164 deletions

View file

@ -19,6 +19,13 @@ actual object DebridSettingsStorage {
private const val torboxApiKeyKey = "debrid_torbox_api_key"
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
private const val instantPlaybackPreparationLimitKey = "debrid_instant_playback_preparation_limit"
private const val streamMaxResultsKey = "debrid_stream_max_results"
private const val streamSortModeKey = "debrid_stream_sort_mode"
private const val streamMinimumQualityKey = "debrid_stream_minimum_quality"
private const val streamDolbyVisionFilterKey = "debrid_stream_dolby_vision_filter"
private const val streamHdrFilterKey = "debrid_stream_hdr_filter"
private const val streamCodecFilterKey = "debrid_stream_codec_filter"
private const val streamPreferencesKey = "debrid_stream_preferences"
private const val streamNameTemplateKey = "debrid_stream_name_template"
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
private val syncKeys = listOf(
@ -26,6 +33,13 @@ actual object DebridSettingsStorage {
torboxApiKeyKey,
realDebridApiKeyKey,
instantPlaybackPreparationLimitKey,
streamMaxResultsKey,
streamSortModeKey,
streamMinimumQualityKey,
streamDolbyVisionFilterKey,
streamHdrFilterKey,
streamCodecFilterKey,
streamPreferencesKey,
streamNameTemplateKey,
streamDescriptionTemplateKey,
)
@ -60,6 +74,48 @@ actual object DebridSettingsStorage {
saveInt(instantPlaybackPreparationLimitKey, limit)
}
actual fun loadStreamMaxResults(): Int? = loadInt(streamMaxResultsKey)
actual fun saveStreamMaxResults(maxResults: Int) {
saveInt(streamMaxResultsKey, maxResults)
}
actual fun loadStreamSortMode(): String? = loadString(streamSortModeKey)
actual fun saveStreamSortMode(mode: String) {
saveString(streamSortModeKey, mode)
}
actual fun loadStreamMinimumQuality(): String? = loadString(streamMinimumQualityKey)
actual fun saveStreamMinimumQuality(quality: String) {
saveString(streamMinimumQualityKey, quality)
}
actual fun loadStreamDolbyVisionFilter(): String? = loadString(streamDolbyVisionFilterKey)
actual fun saveStreamDolbyVisionFilter(filter: String) {
saveString(streamDolbyVisionFilterKey, filter)
}
actual fun loadStreamHdrFilter(): String? = loadString(streamHdrFilterKey)
actual fun saveStreamHdrFilter(filter: String) {
saveString(streamHdrFilterKey, filter)
}
actual fun loadStreamCodecFilter(): String? = loadString(streamCodecFilterKey)
actual fun saveStreamCodecFilter(filter: String) {
saveString(streamCodecFilterKey, filter)
}
actual fun loadStreamPreferences(): String? = loadString(streamPreferencesKey)
actual fun saveStreamPreferences(preferences: String) {
saveString(streamPreferencesKey, preferences)
}
actual fun loadStreamNameTemplate(): String? = loadString(streamNameTemplateKey)
actual fun saveStreamNameTemplate(template: String) {
@ -121,6 +177,13 @@ actual object DebridSettingsStorage {
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(it)) }
loadStreamMaxResults()?.let { put(streamMaxResultsKey, encodeSyncInt(it)) }
loadStreamSortMode()?.let { put(streamSortModeKey, encodeSyncString(it)) }
loadStreamMinimumQuality()?.let { put(streamMinimumQualityKey, encodeSyncString(it)) }
loadStreamDolbyVisionFilter()?.let { put(streamDolbyVisionFilterKey, encodeSyncString(it)) }
loadStreamHdrFilter()?.let { put(streamHdrFilterKey, encodeSyncString(it)) }
loadStreamCodecFilter()?.let { put(streamCodecFilterKey, encodeSyncString(it)) }
loadStreamPreferences()?.let { put(streamPreferencesKey, encodeSyncString(it)) }
loadStreamNameTemplate()?.let { put(streamNameTemplateKey, encodeSyncString(it)) }
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
}
@ -134,6 +197,13 @@ actual object DebridSettingsStorage {
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
payload.decodeSyncInt(streamMaxResultsKey)?.let(::saveStreamMaxResults)
payload.decodeSyncString(streamSortModeKey)?.let(::saveStreamSortMode)
payload.decodeSyncString(streamMinimumQualityKey)?.let(::saveStreamMinimumQuality)
payload.decodeSyncString(streamDolbyVisionFilterKey)?.let(::saveStreamDolbyVisionFilter)
payload.decodeSyncString(streamHdrFilterKey)?.let(::saveStreamHdrFilter)
payload.decodeSyncString(streamCodecFilterKey)?.let(::saveStreamCodecFilter)
payload.decodeSyncString(streamPreferencesKey)?.let(::saveStreamPreferences)
payload.decodeSyncString(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
}

View file

@ -18,6 +18,7 @@
<string name="action_resume">Resume</string>
<string name="action_retry">Retry</string>
<string name="action_save">Save</string>
<string name="action_saving">Saving…</string>
<string name="action_validate">Validate</string>
<string name="addon_installing">Installing</string>
<string name="addon_title">Addons</string>
@ -596,6 +597,10 @@
<string name="settings_debrid_add_key_first">Add an API key first.</string>
<string name="settings_debrid_section_providers">Account</string>
<string name="settings_debrid_provider_torbox_description">Connect your Torbox account.</string>
<string name="settings_debrid_dialog_title">Torbox API Key</string>
<string name="settings_debrid_dialog_subtitle">Enter your Torbox API key.</string>
<string name="settings_debrid_dialog_placeholder">Enter Torbox API key</string>
<string name="settings_debrid_not_set">Not set</string>
<string name="settings_debrid_section_instant_playback">Instant Playback</string>
<string name="settings_debrid_prepare_instant_playback">Prepare links</string>
<string name="settings_debrid_prepare_instant_playback_description">Resolve the first sources before playback starts.</string>
@ -607,6 +612,8 @@
<string name="settings_debrid_name_template_description">Controls how source names appear.</string>
<string name="settings_debrid_description_template">Description template</string>
<string name="settings_debrid_description_template_description">Controls the metadata shown under each source.</string>
<string name="settings_debrid_formatter_reset_title">Reset formatting</string>
<string name="settings_debrid_formatter_reset_subtitle">Restore default source formatting.</string>
<string name="settings_debrid_key_valid">API key validated.</string>
<string name="settings_debrid_key_invalid">Could not validate this API key.</string>
<string name="settings_mdb_add_api_key_first">Add your MDBList API key below before turning ratings on.</string>

View file

@ -1,10 +1,19 @@
package com.nuvio.app.features.debrid
import kotlinx.serialization.Serializable
data class DebridSettings(
val enabled: Boolean = false,
val torboxApiKey: String = "",
val realDebridApiKey: String = "",
val instantPlaybackPreparationLimit: Int = 0,
val streamMaxResults: Int = 0,
val streamSortMode: DebridStreamSortMode = DebridStreamSortMode.DEFAULT,
val streamMinimumQuality: DebridStreamMinimumQuality = DebridStreamMinimumQuality.ANY,
val streamDolbyVisionFilter: DebridStreamFeatureFilter = DebridStreamFeatureFilter.ANY,
val streamHdrFilter: DebridStreamFeatureFilter = DebridStreamFeatureFilter.ANY,
val streamCodecFilter: DebridStreamCodecFilter = DebridStreamCodecFilter.ANY,
val streamPreferences: DebridStreamPreferences = DebridStreamPreferences(),
val streamNameTemplate: String = DebridStreamFormatterDefaults.NAME_TEMPLATE,
val streamDescriptionTemplate: String = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
) {
@ -12,8 +21,236 @@ data class DebridSettings(
get() = DebridProviders.configuredServices(this).isNotEmpty()
}
internal const val DEBRID_PREPARE_INSTANT_PLAYBACK_DEFAULT_LIMIT = 2
internal const val DEBRID_PREPARE_INSTANT_PLAYBACK_MAX_LIMIT = 5
const val DEBRID_PREPARE_INSTANT_PLAYBACK_DEFAULT_LIMIT = 2
const val DEBRID_PREPARE_INSTANT_PLAYBACK_MAX_LIMIT = 5
internal fun normalizeDebridInstantPlaybackPreparationLimit(value: Int): Int =
enum class DebridStreamSortMode {
DEFAULT,
QUALITY_DESC,
SIZE_DESC,
SIZE_ASC,
}
enum class DebridStreamMinimumQuality(val minResolution: Int) {
ANY(0),
P720(720),
P1080(1080),
P2160(2160),
}
enum class DebridStreamFeatureFilter {
ANY,
EXCLUDE,
ONLY,
}
enum class DebridStreamCodecFilter {
ANY,
H264,
HEVC,
AV1,
}
@Serializable
data class DebridStreamPreferences(
val maxResults: Int = 0,
val maxPerResolution: Int = 0,
val maxPerQuality: Int = 0,
val sizeMinGb: Int = 0,
val sizeMaxGb: Int = 0,
val preferredResolutions: List<DebridStreamResolution> = DebridStreamResolution.defaultOrder,
val requiredResolutions: List<DebridStreamResolution> = emptyList(),
val excludedResolutions: List<DebridStreamResolution> = emptyList(),
val preferredQualities: List<DebridStreamQuality> = DebridStreamQuality.defaultOrder,
val requiredQualities: List<DebridStreamQuality> = emptyList(),
val excludedQualities: List<DebridStreamQuality> = emptyList(),
val preferredVisualTags: List<DebridStreamVisualTag> = DebridStreamVisualTag.defaultOrder,
val requiredVisualTags: List<DebridStreamVisualTag> = emptyList(),
val excludedVisualTags: List<DebridStreamVisualTag> = emptyList(),
val preferredAudioTags: List<DebridStreamAudioTag> = DebridStreamAudioTag.defaultOrder,
val requiredAudioTags: List<DebridStreamAudioTag> = emptyList(),
val excludedAudioTags: List<DebridStreamAudioTag> = emptyList(),
val preferredAudioChannels: List<DebridStreamAudioChannel> = DebridStreamAudioChannel.defaultOrder,
val requiredAudioChannels: List<DebridStreamAudioChannel> = emptyList(),
val excludedAudioChannels: List<DebridStreamAudioChannel> = emptyList(),
val preferredEncodes: List<DebridStreamEncode> = DebridStreamEncode.defaultOrder,
val requiredEncodes: List<DebridStreamEncode> = emptyList(),
val excludedEncodes: List<DebridStreamEncode> = emptyList(),
val preferredLanguages: List<DebridStreamLanguage> = emptyList(),
val requiredLanguages: List<DebridStreamLanguage> = emptyList(),
val excludedLanguages: List<DebridStreamLanguage> = emptyList(),
val requiredReleaseGroups: List<String> = emptyList(),
val excludedReleaseGroups: List<String> = emptyList(),
val sortCriteria: List<DebridStreamSortCriterion> = DebridStreamSortCriterion.defaultOrder,
)
@Serializable
enum class DebridStreamResolution(val label: String, val value: Int) {
P2160("2160p", 2160),
P1440("1440p", 1440),
P1080("1080p", 1080),
P720("720p", 720),
P576("576p", 576),
P480("480p", 480),
P360("360p", 360),
UNKNOWN("Unknown", 0);
companion object {
val defaultOrder = listOf(P2160, P1440, P1080, P720, P576, P480, P360, UNKNOWN)
}
}
@Serializable
enum class DebridStreamQuality(val label: String) {
BLURAY_REMUX("BluRay REMUX"),
BLURAY("BluRay"),
WEB_DL("WEB-DL"),
WEBRIP("WEBRip"),
HDRIP("HDRip"),
HD_RIP("HC HD-Rip"),
DVDRIP("DVDRip"),
HDTV("HDTV"),
CAM("CAM"),
TS("TS"),
TC("TC"),
SCR("SCR"),
UNKNOWN("Unknown");
companion object {
val defaultOrder = listOf(BLURAY_REMUX, BLURAY, WEB_DL, WEBRIP, HDRIP, HD_RIP, DVDRIP, HDTV, CAM, TS, TC, SCR, UNKNOWN)
}
}
@Serializable
enum class DebridStreamVisualTag(val label: String) {
HDR_DV("HDR+DV"),
DV_ONLY("DV Only"),
HDR_ONLY("HDR Only"),
HDR10_PLUS("HDR10+"),
HDR10("HDR10"),
DV("DV"),
HDR("HDR"),
HLG("HLG"),
TEN_BIT("10bit"),
THREE_D("3D"),
IMAX("IMAX"),
AI("AI"),
SDR("SDR"),
H_OU("H-OU"),
H_SBS("H-SBS"),
UNKNOWN("Unknown");
companion object {
val defaultOrder = listOf(HDR_DV, DV_ONLY, HDR_ONLY, HDR10_PLUS, HDR10, DV, HDR, HLG, TEN_BIT, IMAX, SDR, THREE_D, AI, H_OU, H_SBS, UNKNOWN)
}
}
@Serializable
enum class DebridStreamAudioTag(val label: String) {
ATMOS("Atmos"),
DD_PLUS("DD+"),
DD("DD"),
DTS_X("DTS:X"),
DTS_HD_MA("DTS-HD MA"),
DTS_HD("DTS-HD"),
DTS_ES("DTS-ES"),
DTS("DTS"),
TRUEHD("TrueHD"),
OPUS("OPUS"),
FLAC("FLAC"),
AAC("AAC"),
UNKNOWN("Unknown");
companion object {
val defaultOrder = listOf(ATMOS, DD_PLUS, DD, DTS_X, DTS_HD_MA, DTS_HD, DTS_ES, DTS, TRUEHD, OPUS, FLAC, AAC, UNKNOWN)
}
}
@Serializable
enum class DebridStreamAudioChannel(val label: String) {
CH_2_0("2.0"),
CH_5_1("5.1"),
CH_6_1("6.1"),
CH_7_1("7.1"),
UNKNOWN("Unknown");
companion object {
val defaultOrder = listOf(CH_7_1, CH_6_1, CH_5_1, CH_2_0, UNKNOWN)
}
}
@Serializable
enum class DebridStreamEncode(val label: String) {
AV1("AV1"),
HEVC("HEVC"),
AVC("AVC"),
XVID("XviD"),
DIVX("DivX"),
UNKNOWN("Unknown");
companion object {
val defaultOrder = listOf(AV1, HEVC, AVC, XVID, DIVX, UNKNOWN)
}
}
@Serializable
enum class DebridStreamLanguage(val code: String, val label: String) {
EN("en", "English"),
HI("hi", "Hindi"),
IT("it", "Italian"),
ES("es", "Spanish"),
FR("fr", "French"),
DE("de", "German"),
PT("pt", "Portuguese"),
PL("pl", "Polish"),
CS("cs", "Czech"),
LA("la", "Latino"),
JA("ja", "Japanese"),
KO("ko", "Korean"),
ZH("zh", "Chinese"),
MULTI("multi", "Multi"),
UNKNOWN("unknown", "Unknown"),
}
@Serializable
data class DebridStreamSortCriterion(
val key: DebridStreamSortKey = DebridStreamSortKey.RESOLUTION,
val direction: DebridStreamSortDirection = DebridStreamSortDirection.DESC,
) {
companion object {
val defaultOrder = listOf(
DebridStreamSortCriterion(DebridStreamSortKey.RESOLUTION, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.QUALITY, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.VISUAL_TAG, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.AUDIO_TAG, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.AUDIO_CHANNEL, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.ENCODE, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.DESC),
)
}
}
@Serializable
enum class DebridStreamSortKey(val label: String) {
RESOLUTION("Resolution"),
QUALITY("Quality"),
VISUAL_TAG("Visual tag"),
AUDIO_TAG("Audio"),
AUDIO_CHANNEL("Audio channel"),
ENCODE("Encode"),
SIZE("Size"),
LANGUAGE("Language"),
RELEASE_GROUP("Release group"),
}
@Serializable
enum class DebridStreamSortDirection {
ASC,
DESC,
}
fun normalizeDebridInstantPlaybackPreparationLimit(value: Int): Int =
value.coerceIn(0, DEBRID_PREPARE_INSTANT_PLAYBACK_MAX_LIMIT)
fun normalizeDebridStreamMaxResults(value: Int): Int =
if (value <= 0) 0 else value.coerceIn(1, 100)

View file

@ -3,16 +3,34 @@ package com.nuvio.app.features.debrid
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
object DebridSettingsRepository {
private val _uiState = MutableStateFlow(DebridSettings())
val uiState: StateFlow<DebridSettings> = _uiState.asStateFlow()
@OptIn(ExperimentalSerializationApi::class)
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
private var hasLoaded = false
private var enabled = false
private var torboxApiKey = ""
private var realDebridApiKey = ""
private var instantPlaybackPreparationLimit = 0
private var streamMaxResults = 0
private var streamSortMode = DebridStreamSortMode.DEFAULT
private var streamMinimumQuality = DebridStreamMinimumQuality.ANY
private var streamDolbyVisionFilter = DebridStreamFeatureFilter.ANY
private var streamHdrFilter = DebridStreamFeatureFilter.ANY
private var streamCodecFilter = DebridStreamCodecFilter.ANY
private var streamPreferences = DebridStreamPreferences()
private var streamNameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE
private var streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
@ -68,6 +86,78 @@ object DebridSettingsRepository {
DebridSettingsStorage.saveInstantPlaybackPreparationLimit(normalized)
}
fun setStreamMaxResults(value: Int) {
ensureLoaded()
val normalized = normalizeDebridStreamMaxResults(value)
if (streamMaxResults == normalized && streamPreferences.maxResults == normalized) return
streamMaxResults = normalized
streamPreferences = streamPreferences.copy(maxResults = normalized).normalized()
publish()
DebridSettingsStorage.saveStreamMaxResults(normalized)
saveStreamPreferences()
}
fun setStreamSortMode(value: DebridStreamSortMode) {
ensureLoaded()
if (streamSortMode == value && streamPreferences.sortCriteria == sortCriteriaForLegacyMode(value)) return
streamSortMode = value
streamPreferences = streamPreferences.copy(sortCriteria = sortCriteriaForLegacyMode(value)).normalized()
publish()
DebridSettingsStorage.saveStreamSortMode(value.name)
saveStreamPreferences()
}
fun setStreamMinimumQuality(value: DebridStreamMinimumQuality) {
ensureLoaded()
if (streamMinimumQuality == value && streamPreferences.requiredResolutions == resolutionsForMinimumQuality(value)) return
streamMinimumQuality = value
streamPreferences = streamPreferences.copy(requiredResolutions = resolutionsForMinimumQuality(value)).normalized()
publish()
DebridSettingsStorage.saveStreamMinimumQuality(value.name)
saveStreamPreferences()
}
fun setStreamDolbyVisionFilter(value: DebridStreamFeatureFilter) {
ensureLoaded()
if (streamDolbyVisionFilter == value) return
streamDolbyVisionFilter = value
streamPreferences = streamPreferences.applyDolbyVisionFilter(value).normalized()
publish()
DebridSettingsStorage.saveStreamDolbyVisionFilter(value.name)
saveStreamPreferences()
}
fun setStreamHdrFilter(value: DebridStreamFeatureFilter) {
ensureLoaded()
if (streamHdrFilter == value) return
streamHdrFilter = value
streamPreferences = streamPreferences.applyHdrFilter(value).normalized()
publish()
DebridSettingsStorage.saveStreamHdrFilter(value.name)
saveStreamPreferences()
}
fun setStreamCodecFilter(value: DebridStreamCodecFilter) {
ensureLoaded()
if (streamCodecFilter == value) return
streamCodecFilter = value
streamPreferences = streamPreferences.applyCodecFilter(value).normalized()
publish()
DebridSettingsStorage.saveStreamCodecFilter(value.name)
saveStreamPreferences()
}
fun setStreamPreferences(value: DebridStreamPreferences) {
ensureLoaded()
val normalized = value.normalized()
if (streamPreferences == normalized) return
streamPreferences = normalized
streamMaxResults = normalized.maxResults
publish()
DebridSettingsStorage.saveStreamMaxResults(streamMaxResults)
saveStreamPreferences()
}
fun setStreamNameTemplate(value: String) {
ensureLoaded()
val normalized = value.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
@ -86,15 +176,22 @@ object DebridSettingsRepository {
DebridSettingsStorage.saveStreamDescriptionTemplate(normalized)
}
fun resetStreamTemplates() {
fun setStreamTemplates(nameTemplate: String, descriptionTemplate: String) {
ensureLoaded()
streamNameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE
streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
streamNameTemplate = nameTemplate.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
streamDescriptionTemplate = descriptionTemplate.ifBlank { DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE }
publish()
DebridSettingsStorage.saveStreamNameTemplate(streamNameTemplate)
DebridSettingsStorage.saveStreamDescriptionTemplate(streamDescriptionTemplate)
}
fun resetStreamTemplates() {
setStreamTemplates(
nameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE,
descriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
)
}
private fun disableIfNoKeys() {
if (!hasVisibleApiKey()) {
enabled = false
@ -114,6 +211,36 @@ object DebridSettingsRepository {
instantPlaybackPreparationLimit = normalizeDebridInstantPlaybackPreparationLimit(
DebridSettingsStorage.loadInstantPlaybackPreparationLimit() ?: 0,
)
streamMaxResults = normalizeDebridStreamMaxResults(DebridSettingsStorage.loadStreamMaxResults() ?: 0)
streamSortMode = enumValueOrDefault(
DebridSettingsStorage.loadStreamSortMode(),
DebridStreamSortMode.DEFAULT,
)
streamMinimumQuality = enumValueOrDefault(
DebridSettingsStorage.loadStreamMinimumQuality(),
DebridStreamMinimumQuality.ANY,
)
streamDolbyVisionFilter = enumValueOrDefault(
DebridSettingsStorage.loadStreamDolbyVisionFilter(),
DebridStreamFeatureFilter.ANY,
)
streamHdrFilter = enumValueOrDefault(
DebridSettingsStorage.loadStreamHdrFilter(),
DebridStreamFeatureFilter.ANY,
)
streamCodecFilter = enumValueOrDefault(
DebridSettingsStorage.loadStreamCodecFilter(),
DebridStreamCodecFilter.ANY,
)
streamPreferences = parseStreamPreferences(DebridSettingsStorage.loadStreamPreferences())
?: legacyStreamPreferences(
maxResults = streamMaxResults,
sortMode = streamSortMode,
minimumQuality = streamMinimumQuality,
dolbyVisionFilter = streamDolbyVisionFilter,
hdrFilter = streamHdrFilter,
codecFilter = streamCodecFilter,
)
streamNameTemplate = DebridSettingsStorage.loadStreamNameTemplate()
?.takeIf { it.isNotBlank() }
?: DebridStreamFormatterDefaults.NAME_TEMPLATE
@ -129,8 +256,164 @@ object DebridSettingsRepository {
torboxApiKey = torboxApiKey,
realDebridApiKey = realDebridApiKey,
instantPlaybackPreparationLimit = instantPlaybackPreparationLimit,
streamMaxResults = streamMaxResults,
streamSortMode = streamSortMode,
streamMinimumQuality = streamMinimumQuality,
streamDolbyVisionFilter = streamDolbyVisionFilter,
streamHdrFilter = streamHdrFilter,
streamCodecFilter = streamCodecFilter,
streamPreferences = streamPreferences,
streamNameTemplate = streamNameTemplate,
streamDescriptionTemplate = streamDescriptionTemplate,
)
}
private fun saveStreamPreferences() {
DebridSettingsStorage.saveStreamPreferences(json.encodeToString(streamPreferences.normalized()))
}
private inline fun <reified T : Enum<T>> enumValueOrDefault(value: String?, default: T): T =
runCatching { enumValueOf<T>(value.orEmpty()) }.getOrDefault(default)
private fun parseStreamPreferences(value: String?): DebridStreamPreferences? {
if (value.isNullOrBlank()) return null
return try {
json.decodeFromString<DebridStreamPreferences>(value).normalized()
} catch (_: SerializationException) {
null
} catch (_: IllegalArgumentException) {
null
}
}
}
internal fun DebridStreamPreferences.normalized(): DebridStreamPreferences =
copy(
maxResults = normalizeDebridStreamMaxResults(maxResults),
maxPerResolution = maxPerResolution.coerceIn(0, 100),
maxPerQuality = maxPerQuality.coerceIn(0, 100),
sizeMinGb = sizeMinGb.coerceIn(0, 100),
sizeMaxGb = sizeMaxGb.coerceIn(0, 100),
preferredResolutions = preferredResolutions.ifEmpty { DebridStreamResolution.defaultOrder },
requiredResolutions = requiredResolutions,
excludedResolutions = excludedResolutions,
preferredQualities = preferredQualities.ifEmpty { DebridStreamQuality.defaultOrder },
requiredQualities = requiredQualities,
excludedQualities = excludedQualities,
preferredVisualTags = preferredVisualTags.ifEmpty { DebridStreamVisualTag.defaultOrder },
requiredVisualTags = requiredVisualTags,
excludedVisualTags = excludedVisualTags,
preferredAudioTags = preferredAudioTags.ifEmpty { DebridStreamAudioTag.defaultOrder },
requiredAudioTags = requiredAudioTags,
excludedAudioTags = excludedAudioTags,
preferredAudioChannels = preferredAudioChannels.ifEmpty { DebridStreamAudioChannel.defaultOrder },
requiredAudioChannels = requiredAudioChannels,
excludedAudioChannels = excludedAudioChannels,
preferredEncodes = preferredEncodes.ifEmpty { DebridStreamEncode.defaultOrder },
requiredEncodes = requiredEncodes,
excludedEncodes = excludedEncodes,
preferredLanguages = preferredLanguages,
requiredLanguages = requiredLanguages,
excludedLanguages = excludedLanguages,
requiredReleaseGroups = requiredReleaseGroups.map { it.trim() }.filter { it.isNotBlank() }.distinct(),
excludedReleaseGroups = excludedReleaseGroups.map { it.trim() }.filter { it.isNotBlank() }.distinct(),
sortCriteria = sortCriteria.ifEmpty { DebridStreamSortCriterion.defaultOrder },
)
private fun legacyStreamPreferences(
maxResults: Int,
sortMode: DebridStreamSortMode,
minimumQuality: DebridStreamMinimumQuality,
dolbyVisionFilter: DebridStreamFeatureFilter,
hdrFilter: DebridStreamFeatureFilter,
codecFilter: DebridStreamCodecFilter,
): DebridStreamPreferences =
DebridStreamPreferences(
maxResults = normalizeDebridStreamMaxResults(maxResults),
sortCriteria = sortCriteriaForLegacyMode(sortMode),
requiredResolutions = resolutionsForMinimumQuality(minimumQuality),
)
.applyDolbyVisionFilter(dolbyVisionFilter)
.applyHdrFilter(hdrFilter)
.applyCodecFilter(codecFilter)
.normalized()
private fun DebridStreamPreferences.applyDolbyVisionFilter(
filter: DebridStreamFeatureFilter,
): DebridStreamPreferences =
when (filter) {
DebridStreamFeatureFilter.ANY -> copy(
requiredVisualTags = requiredVisualTags - dolbyVisionTags.toSet(),
excludedVisualTags = excludedVisualTags - dolbyVisionTags.toSet(),
)
DebridStreamFeatureFilter.EXCLUDE -> copy(
requiredVisualTags = requiredVisualTags - dolbyVisionTags.toSet(),
excludedVisualTags = (excludedVisualTags + dolbyVisionTags).distinct(),
)
DebridStreamFeatureFilter.ONLY -> copy(
requiredVisualTags = (requiredVisualTags + dolbyVisionTags).distinct(),
excludedVisualTags = excludedVisualTags - dolbyVisionTags.toSet(),
)
}
private fun DebridStreamPreferences.applyHdrFilter(
filter: DebridStreamFeatureFilter,
): DebridStreamPreferences =
when (filter) {
DebridStreamFeatureFilter.ANY -> copy(
requiredVisualTags = requiredVisualTags - hdrTags.toSet(),
excludedVisualTags = excludedVisualTags - hdrTags.toSet(),
)
DebridStreamFeatureFilter.EXCLUDE -> copy(
requiredVisualTags = requiredVisualTags - hdrTags.toSet(),
excludedVisualTags = (excludedVisualTags + hdrTags).distinct(),
)
DebridStreamFeatureFilter.ONLY -> copy(
requiredVisualTags = (requiredVisualTags + hdrTags).distinct(),
excludedVisualTags = excludedVisualTags - hdrTags.toSet(),
)
}
private fun DebridStreamPreferences.applyCodecFilter(
filter: DebridStreamCodecFilter,
): DebridStreamPreferences =
copy(
requiredEncodes = when (filter) {
DebridStreamCodecFilter.ANY -> emptyList()
DebridStreamCodecFilter.H264 -> listOf(DebridStreamEncode.AVC)
DebridStreamCodecFilter.HEVC -> listOf(DebridStreamEncode.HEVC)
DebridStreamCodecFilter.AV1 -> listOf(DebridStreamEncode.AV1)
},
)
private fun resolutionsForMinimumQuality(quality: DebridStreamMinimumQuality): List<DebridStreamResolution> =
DebridStreamResolution.defaultOrder.filter {
it.value >= quality.minResolution && it != DebridStreamResolution.UNKNOWN
}
private fun sortCriteriaForLegacyMode(mode: DebridStreamSortMode): List<DebridStreamSortCriterion> =
when (mode) {
DebridStreamSortMode.DEFAULT -> DebridStreamSortCriterion.defaultOrder
DebridStreamSortMode.QUALITY_DESC -> listOf(
DebridStreamSortCriterion(DebridStreamSortKey.RESOLUTION, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.QUALITY, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.DESC),
)
DebridStreamSortMode.SIZE_DESC -> listOf(DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.DESC))
DebridStreamSortMode.SIZE_ASC -> listOf(DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.ASC))
}
private val dolbyVisionTags = listOf(
DebridStreamVisualTag.DV,
DebridStreamVisualTag.DV_ONLY,
DebridStreamVisualTag.HDR_DV,
)
private val hdrTags = listOf(
DebridStreamVisualTag.HDR,
DebridStreamVisualTag.HDR10,
DebridStreamVisualTag.HDR10_PLUS,
DebridStreamVisualTag.HLG,
DebridStreamVisualTag.HDR_ONLY,
DebridStreamVisualTag.HDR_DV,
)

View file

@ -11,6 +11,20 @@ internal expect object DebridSettingsStorage {
fun saveRealDebridApiKey(apiKey: String)
fun loadInstantPlaybackPreparationLimit(): Int?
fun saveInstantPlaybackPreparationLimit(limit: Int)
fun loadStreamMaxResults(): Int?
fun saveStreamMaxResults(maxResults: Int)
fun loadStreamSortMode(): String?
fun saveStreamSortMode(mode: String)
fun loadStreamMinimumQuality(): String?
fun saveStreamMinimumQuality(quality: String)
fun loadStreamDolbyVisionFilter(): String?
fun saveStreamDolbyVisionFilter(filter: String)
fun loadStreamHdrFilter(): String?
fun saveStreamHdrFilter(filter: String)
fun loadStreamCodecFilter(): String?
fun saveStreamCodecFilter(filter: String)
fun loadStreamPreferences(): String?
fun saveStreamPreferences(preferences: String)
fun loadStreamNameTemplate(): String?
fun saveStreamNameTemplate(template: String)
fun loadStreamDescriptionTemplate(): String?

View file

@ -5,8 +5,8 @@ import com.nuvio.app.features.streams.StreamItem
object DirectDebridStreamFilter {
const val FALLBACK_SOURCE_NAME = "Direct Debrid"
fun filterInstant(streams: List<StreamItem>): List<StreamItem> =
streams
fun filterInstant(streams: List<StreamItem>, settings: DebridSettings? = null): List<StreamItem> {
val instantStreams = streams
.filter(::isInstantCandidate)
.map { stream ->
val providerId = stream.clientResolve?.service
@ -27,6 +27,8 @@ object DirectDebridStreamFilter {
stream.title,
).joinToString("|")
}
return if (settings == null) instantStreams else applyPreferences(instantStreams, settings)
}
fun isInstantCandidate(stream: StreamItem): Boolean {
val resolve = stream.clientResolve ?: return false
@ -37,5 +39,387 @@ object DirectDebridStreamFilter {
fun isDirectDebridSourceName(addonName: String): Boolean =
DebridProviders.all().any { addonName == DebridProviders.instantName(it.id) }
}
private fun applyPreferences(streams: List<StreamItem>, settings: DebridSettings): List<StreamItem> {
val preferences = effectivePreferences(settings)
return streams.map { it to streamFacts(it, preferences) }
.filter { (_, facts) -> facts.matchesFilters(preferences) }
.sortedWith { left, right -> compareFacts(left.second, right.second, preferences.sortCriteria) }
.let { sorted -> applyLimits(sorted, preferences) }
.map { it.first }
}
private fun effectivePreferences(settings: DebridSettings): DebridStreamPreferences {
val default = DebridStreamPreferences()
if (settings.streamPreferences != default) return settings.streamPreferences.normalized()
if (
settings.streamMaxResults == 0 &&
settings.streamSortMode == DebridStreamSortMode.DEFAULT &&
settings.streamMinimumQuality == DebridStreamMinimumQuality.ANY &&
settings.streamDolbyVisionFilter == DebridStreamFeatureFilter.ANY &&
settings.streamHdrFilter == DebridStreamFeatureFilter.ANY &&
settings.streamCodecFilter == DebridStreamCodecFilter.ANY
) {
return default
}
var preferences = default.copy(
maxResults = settings.streamMaxResults,
sortCriteria = when (settings.streamSortMode) {
DebridStreamSortMode.DEFAULT -> default.sortCriteria
DebridStreamSortMode.QUALITY_DESC -> listOf(
DebridStreamSortCriterion(DebridStreamSortKey.RESOLUTION, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.QUALITY, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.DESC),
)
DebridStreamSortMode.SIZE_DESC -> listOf(DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.DESC))
DebridStreamSortMode.SIZE_ASC -> listOf(DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.ASC))
},
requiredResolutions = DebridStreamResolution.defaultOrder.filter {
it.value >= settings.streamMinimumQuality.minResolution && it != DebridStreamResolution.UNKNOWN
},
)
preferences = when (settings.streamDolbyVisionFilter) {
DebridStreamFeatureFilter.ANY -> preferences
DebridStreamFeatureFilter.EXCLUDE -> preferences.copy(
excludedVisualTags = preferences.excludedVisualTags + listOf(
DebridStreamVisualTag.DV,
DebridStreamVisualTag.DV_ONLY,
DebridStreamVisualTag.HDR_DV,
),
)
DebridStreamFeatureFilter.ONLY -> preferences.copy(
requiredVisualTags = preferences.requiredVisualTags + listOf(
DebridStreamVisualTag.DV,
DebridStreamVisualTag.DV_ONLY,
DebridStreamVisualTag.HDR_DV,
),
)
}
preferences = when (settings.streamHdrFilter) {
DebridStreamFeatureFilter.ANY -> preferences
DebridStreamFeatureFilter.EXCLUDE -> preferences.copy(
excludedVisualTags = preferences.excludedVisualTags + listOf(
DebridStreamVisualTag.HDR,
DebridStreamVisualTag.HDR10,
DebridStreamVisualTag.HDR10_PLUS,
DebridStreamVisualTag.HLG,
DebridStreamVisualTag.HDR_ONLY,
DebridStreamVisualTag.HDR_DV,
),
)
DebridStreamFeatureFilter.ONLY -> preferences.copy(
requiredVisualTags = preferences.requiredVisualTags + listOf(
DebridStreamVisualTag.HDR,
DebridStreamVisualTag.HDR10,
DebridStreamVisualTag.HDR10_PLUS,
DebridStreamVisualTag.HLG,
DebridStreamVisualTag.HDR_ONLY,
DebridStreamVisualTag.HDR_DV,
),
)
}
return when (settings.streamCodecFilter) {
DebridStreamCodecFilter.ANY -> preferences
DebridStreamCodecFilter.H264 -> preferences.copy(requiredEncodes = listOf(DebridStreamEncode.AVC))
DebridStreamCodecFilter.HEVC -> preferences.copy(requiredEncodes = listOf(DebridStreamEncode.HEVC))
DebridStreamCodecFilter.AV1 -> preferences.copy(requiredEncodes = listOf(DebridStreamEncode.AV1))
}.normalized()
}
private fun applyLimits(
streams: List<Pair<StreamItem, StreamFacts>>,
preferences: DebridStreamPreferences,
): List<Pair<StreamItem, StreamFacts>> {
val resolutionCounts = mutableMapOf<DebridStreamResolution, Int>()
val qualityCounts = mutableMapOf<DebridStreamQuality, Int>()
val result = mutableListOf<Pair<StreamItem, StreamFacts>>()
for (stream in streams) {
if (preferences.maxResults > 0 && result.size >= preferences.maxResults) break
if (preferences.maxPerResolution > 0) {
val count = resolutionCounts[stream.second.resolution] ?: 0
if (count >= preferences.maxPerResolution) continue
}
if (preferences.maxPerQuality > 0) {
val count = qualityCounts[stream.second.quality] ?: 0
if (count >= preferences.maxPerQuality) continue
}
resolutionCounts[stream.second.resolution] = (resolutionCounts[stream.second.resolution] ?: 0) + 1
qualityCounts[stream.second.quality] = (qualityCounts[stream.second.quality] ?: 0) + 1
result += stream
}
return result
}
private fun StreamFacts.matchesFilters(preferences: DebridStreamPreferences): Boolean {
if (preferences.requiredResolutions.isNotEmpty() && resolution !in preferences.requiredResolutions) return false
if (resolution in preferences.excludedResolutions) return false
if (preferences.requiredQualities.isNotEmpty() && quality !in preferences.requiredQualities) return false
if (quality in preferences.excludedQualities) return false
if (preferences.requiredVisualTags.isNotEmpty() && visualTags.none { it in preferences.requiredVisualTags }) return false
if (visualTags.any { it in preferences.excludedVisualTags }) return false
if (preferences.requiredAudioTags.isNotEmpty() && audioTags.none { it in preferences.requiredAudioTags }) return false
if (audioTags.any { it in preferences.excludedAudioTags }) return false
if (preferences.requiredAudioChannels.isNotEmpty() && audioChannels.none { it in preferences.requiredAudioChannels }) return false
if (audioChannels.any { it in preferences.excludedAudioChannels }) return false
if (preferences.requiredEncodes.isNotEmpty() && encode !in preferences.requiredEncodes) return false
if (encode in preferences.excludedEncodes) return false
if (preferences.requiredLanguages.isNotEmpty() && languages.none { it in preferences.requiredLanguages }) return false
if (languages.isNotEmpty() && languages.all { it in preferences.excludedLanguages }) return false
if (preferences.requiredReleaseGroups.isNotEmpty() && preferences.requiredReleaseGroups.none { releaseGroup.equals(it, ignoreCase = true) }) return false
if (preferences.excludedReleaseGroups.any { releaseGroup.equals(it, ignoreCase = true) }) return false
if (preferences.sizeMinGb > 0 && size != null && size < preferences.sizeMinGb.gigabytes()) return false
if (preferences.sizeMaxGb > 0 && size != null && size > preferences.sizeMaxGb.gigabytes()) return false
return true
}
private fun compareFacts(
left: StreamFacts,
right: StreamFacts,
criteria: List<DebridStreamSortCriterion>,
): Int {
for (criterion in criteria.ifEmpty { DebridStreamSortCriterion.defaultOrder }) {
val comparison = compareKey(left, right, criterion)
if (comparison != 0) return comparison
}
return 0
}
private fun compareKey(
left: StreamFacts,
right: StreamFacts,
criterion: DebridStreamSortCriterion,
): Int {
val direction = if (criterion.direction == DebridStreamSortDirection.ASC) 1 else -1
return when (criterion.key) {
DebridStreamSortKey.RESOLUTION -> left.resolutionRank.compareTo(right.resolutionRank) * -direction
DebridStreamSortKey.QUALITY -> left.qualityRank.compareTo(right.qualityRank) * -direction
DebridStreamSortKey.VISUAL_TAG -> left.visualRank.compareTo(right.visualRank) * -direction
DebridStreamSortKey.AUDIO_TAG -> left.audioRank.compareTo(right.audioRank) * -direction
DebridStreamSortKey.AUDIO_CHANNEL -> left.channelRank.compareTo(right.channelRank) * -direction
DebridStreamSortKey.ENCODE -> left.encodeRank.compareTo(right.encodeRank) * -direction
DebridStreamSortKey.SIZE -> (left.size ?: 0L).compareTo(right.size ?: 0L) * direction
DebridStreamSortKey.LANGUAGE -> left.languageRank.compareTo(right.languageRank) * -direction
DebridStreamSortKey.RELEASE_GROUP -> left.releaseGroup.compareTo(right.releaseGroup, ignoreCase = true)
}
}
private fun streamFacts(stream: StreamItem, preferences: DebridStreamPreferences): StreamFacts {
val parsed = stream.clientResolve?.stream?.raw?.parsed
val searchText = streamSearchText(stream)
val resolution = streamResolution(parsed?.resolution, parsed?.quality, searchText)
val quality = streamQuality(parsed?.quality, searchText)
val visualTags = streamVisualTags(parsed?.hdr.orEmpty(), searchText)
val audioTags = streamAudioTags(parsed?.audio.orEmpty(), searchText)
val audioChannels = streamAudioChannels(parsed?.channels.orEmpty(), searchText)
val encode = streamEncode(parsed?.codec, searchText)
val languages = parsed?.languages.orEmpty().mapNotNull { languageFor(it) }.ifEmpty {
DebridStreamLanguage.entries.filter { searchText.hasToken(it.code) }
}
val releaseGroup = parsed?.group?.takeIf { it.isNotBlank() } ?: releaseGroupFromText(searchText)
return StreamFacts(
resolution = resolution,
quality = quality,
visualTags = visualTags,
audioTags = audioTags,
audioChannels = audioChannels,
encode = encode,
languages = languages,
releaseGroup = releaseGroup,
size = streamSize(stream),
resolutionRank = rank(resolution, preferences.preferredResolutions),
qualityRank = rank(quality, preferences.preferredQualities),
visualRank = rankAny(visualTags, preferences.preferredVisualTags),
audioRank = rankAny(audioTags, preferences.preferredAudioTags),
channelRank = rankAny(audioChannels, preferences.preferredAudioChannels),
encodeRank = rank(encode, preferences.preferredEncodes),
languageRank = if (languages.isEmpty()) Int.MAX_VALUE else languages.minOf { rank(it, preferences.preferredLanguages) },
)
}
private fun streamResolution(vararg values: String?): DebridStreamResolution =
values.firstNotNullOfOrNull { resolutionValue(it) } ?: DebridStreamResolution.UNKNOWN
private fun resolutionValue(value: String?): DebridStreamResolution? {
val normalized = value?.lowercase().orEmpty()
return when {
normalized.hasResolutionToken("2160p?", "4k", "uhd") -> DebridStreamResolution.P2160
normalized.hasResolutionToken("1440p?", "2k") -> DebridStreamResolution.P1440
normalized.hasResolutionToken("1080p?", "fhd") -> DebridStreamResolution.P1080
normalized.hasResolutionToken("720p?", "hd") -> DebridStreamResolution.P720
normalized.hasResolutionToken("576p?") -> DebridStreamResolution.P576
normalized.hasResolutionToken("480p?", "sd") -> DebridStreamResolution.P480
normalized.hasResolutionToken("360p?") -> DebridStreamResolution.P360
else -> null
}
}
private fun streamQuality(parsedQuality: String?, searchText: String): DebridStreamQuality {
val text = listOfNotNull(parsedQuality, searchText).joinToString(" ").lowercase()
return when {
text.contains("remux") -> DebridStreamQuality.BLURAY_REMUX
text.contains("blu-ray") || text.contains("bluray") || text.contains("bdrip") || text.contains("brrip") -> DebridStreamQuality.BLURAY
text.contains("web-dl") || text.contains("webdl") -> DebridStreamQuality.WEB_DL
text.contains("webrip") || text.contains("web-rip") -> DebridStreamQuality.WEBRIP
text.contains("hdrip") -> DebridStreamQuality.HDRIP
text.contains("hd-rip") || text.contains("hcrip") -> DebridStreamQuality.HD_RIP
text.contains("dvdrip") -> DebridStreamQuality.DVDRIP
text.contains("hdtv") -> DebridStreamQuality.HDTV
text.hasToken("cam") -> DebridStreamQuality.CAM
text.hasToken("ts") -> DebridStreamQuality.TS
text.hasToken("tc") -> DebridStreamQuality.TC
text.hasToken("scr") -> DebridStreamQuality.SCR
else -> DebridStreamQuality.UNKNOWN
}
}
private fun streamVisualTags(parsedHdr: List<String>, searchText: String): List<DebridStreamVisualTag> {
val text = (parsedHdr + searchText).joinToString(" ").lowercase()
val tags = mutableListOf<DebridStreamVisualTag>()
val hasDv = parsedHdr.any { it.isDolbyVisionToken() } ||
Regex("(^|[^a-z0-9])(dv|dovi|dolby[ ._-]?vision)([^a-z0-9]|$)").containsMatchIn(searchText)
val hasHdr = parsedHdr.any { it.isHdrToken() } ||
Regex("(^|[^a-z0-9])(hdr|hdr10|hdr10plus|hdr10\\+|hlg)([^a-z0-9]|$)").containsMatchIn(searchText)
if (hasDv && hasHdr) tags += DebridStreamVisualTag.HDR_DV
if (hasDv && !hasHdr) tags += DebridStreamVisualTag.DV_ONLY
if (hasHdr && !hasDv) tags += DebridStreamVisualTag.HDR_ONLY
if (text.contains("hdr10+") || text.contains("hdr10plus")) tags += DebridStreamVisualTag.HDR10_PLUS
if (text.contains("hdr10")) tags += DebridStreamVisualTag.HDR10
if (hasDv) tags += DebridStreamVisualTag.DV
if (hasHdr) tags += DebridStreamVisualTag.HDR
if (text.hasToken("hlg")) tags += DebridStreamVisualTag.HLG
if (text.contains("10bit") || text.contains("10 bit")) tags += DebridStreamVisualTag.TEN_BIT
if (text.hasToken("3d")) tags += DebridStreamVisualTag.THREE_D
if (text.hasToken("imax")) tags += DebridStreamVisualTag.IMAX
if (text.hasToken("ai")) tags += DebridStreamVisualTag.AI
if (text.hasToken("sdr")) tags += DebridStreamVisualTag.SDR
if (text.contains("h-ou")) tags += DebridStreamVisualTag.H_OU
if (text.contains("h-sbs")) tags += DebridStreamVisualTag.H_SBS
return tags.distinct().ifEmpty { listOf(DebridStreamVisualTag.UNKNOWN) }
}
private fun streamAudioTags(parsedAudio: List<String>, searchText: String): List<DebridStreamAudioTag> {
val text = (parsedAudio + searchText).joinToString(" ").lowercase()
val tags = mutableListOf<DebridStreamAudioTag>()
if (text.hasToken("atmos")) tags += DebridStreamAudioTag.ATMOS
if (text.contains("dd+") || text.contains("ddp") || text.contains("dolby digital plus")) tags += DebridStreamAudioTag.DD_PLUS
if (text.hasToken("dd") || text.contains("ac3") || text.contains("dolby digital")) tags += DebridStreamAudioTag.DD
if (text.contains("dts:x") || text.contains("dtsx")) tags += DebridStreamAudioTag.DTS_X
if (text.contains("dts-hd ma") || text.contains("dtshd ma")) tags += DebridStreamAudioTag.DTS_HD_MA
if (text.contains("dts-hd") || text.contains("dtshd")) tags += DebridStreamAudioTag.DTS_HD
if (text.contains("dts-es") || text.contains("dtses")) tags += DebridStreamAudioTag.DTS_ES
if (text.hasToken("dts")) tags += DebridStreamAudioTag.DTS
if (text.contains("truehd") || text.contains("true hd")) tags += DebridStreamAudioTag.TRUEHD
if (text.hasToken("opus")) tags += DebridStreamAudioTag.OPUS
if (text.hasToken("flac")) tags += DebridStreamAudioTag.FLAC
if (text.hasToken("aac")) tags += DebridStreamAudioTag.AAC
return tags.distinct().ifEmpty { listOf(DebridStreamAudioTag.UNKNOWN) }
}
private fun streamAudioChannels(parsedChannels: List<String>, searchText: String): List<DebridStreamAudioChannel> {
val text = (parsedChannels + searchText).joinToString(" ").lowercase()
val channels = mutableListOf<DebridStreamAudioChannel>()
if (text.hasToken("7.1")) channels += DebridStreamAudioChannel.CH_7_1
if (text.hasToken("6.1")) channels += DebridStreamAudioChannel.CH_6_1
if (text.hasToken("5.1") || text.hasToken("6ch")) channels += DebridStreamAudioChannel.CH_5_1
if (text.hasToken("2.0")) channels += DebridStreamAudioChannel.CH_2_0
return channels.distinct().ifEmpty { listOf(DebridStreamAudioChannel.UNKNOWN) }
}
private fun streamEncode(parsedCodec: String?, searchText: String): DebridStreamEncode {
val text = listOfNotNull(parsedCodec, searchText).joinToString(" ").lowercase()
return when {
text.hasToken("av1") -> DebridStreamEncode.AV1
text.hasToken("hevc") || text.hasToken("h265") || text.hasToken("x265") -> DebridStreamEncode.HEVC
text.hasToken("avc") || text.hasToken("h264") || text.hasToken("x264") -> DebridStreamEncode.AVC
text.hasToken("xvid") -> DebridStreamEncode.XVID
text.hasToken("divx") -> DebridStreamEncode.DIVX
else -> DebridStreamEncode.UNKNOWN
}
}
private fun languageFor(value: String): DebridStreamLanguage? {
val normalized = value.lowercase()
return DebridStreamLanguage.entries.firstOrNull {
normalized == it.code || normalized == it.label.lowercase()
}
}
private fun releaseGroupFromText(text: String): String =
Regex("-([a-z0-9][a-z0-9._]{1,24})($|\\.)", RegexOption.IGNORE_CASE)
.find(text)
?.groupValues
?.getOrNull(1)
.orEmpty()
private fun <T> rank(value: T, preferred: List<T>): Int {
val index = preferred.indexOf(value)
return if (index >= 0) index else Int.MAX_VALUE
}
private fun <T> rankAny(values: List<T>, preferred: List<T>): Int =
values.minOfOrNull { rank(it, preferred) } ?: Int.MAX_VALUE
private fun String.hasResolutionToken(vararg tokens: String): Boolean =
Regex("(^|[^a-z0-9])(${tokens.joinToString("|")})([^a-z0-9]|\$)").containsMatchIn(this)
private fun String.hasToken(token: String): Boolean =
Regex("(^|[^a-z0-9])${Regex.escape(token.lowercase())}([^a-z0-9]|\$)").containsMatchIn(lowercase())
private fun String.isDolbyVisionToken(): Boolean {
val normalized = lowercase().replace(Regex("[^a-z0-9]"), "")
return normalized == "dv" || normalized == "dovi" || normalized == "dolbyvision"
}
private fun String.isHdrToken(): Boolean {
val normalized = lowercase().replace(Regex("[^a-z0-9+]"), "")
return normalized == "hdr" ||
normalized == "hdr10" ||
normalized == "hdr10+" ||
normalized == "hdr10plus" ||
normalized == "hlg"
}
private fun streamSize(stream: StreamItem): Long? =
stream.clientResolve?.stream?.raw?.size ?: stream.behaviorHints.videoSize
private fun streamSearchText(stream: StreamItem): String {
val resolve = stream.clientResolve
val raw = resolve?.stream?.raw
val parsed = raw?.parsed
return listOfNotNull(
stream.name,
stream.title,
stream.description,
resolve?.torrentName,
resolve?.filename,
raw?.torrentName,
raw?.filename,
parsed?.resolution,
parsed?.quality,
parsed?.codec,
parsed?.hdr?.joinToString(" "),
parsed?.audio?.joinToString(" "),
).joinToString(" ").lowercase()
}
private fun Int.gigabytes(): Long = this * 1_000_000_000L
private data class StreamFacts(
val resolution: DebridStreamResolution,
val quality: DebridStreamQuality,
val visualTags: List<DebridStreamVisualTag>,
val audioTags: List<DebridStreamAudioTag>,
val audioChannels: List<DebridStreamAudioChannel>,
val encode: DebridStreamEncode,
val languages: List<DebridStreamLanguage>,
val releaseGroup: String,
val size: Long?,
val resolutionRank: Int,
val qualityRank: Int,
val visualRank: Int,
val audioRank: Int,
val channelRank: Int,
val encodeRank: Int,
val languageRank: Int,
)
}

View file

@ -4,9 +4,20 @@ import co.touchlab.kermit.Logger
import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.streams.AddonStreamGroup
import com.nuvio.app.features.streams.StreamParser
import com.nuvio.app.features.streams.epochMs
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private const val DIRECT_DEBRID_TAG = "DirectDebridStreams"
private const val STREAM_CACHE_TTL_MS = 5L * 60L * 1000L
data class DirectDebridStreamTarget(
val provider: DebridProvider,
@ -20,6 +31,10 @@ object DirectDebridStreamSource {
private val log = Logger.withTag(DIRECT_DEBRID_TAG)
private val encoder = DirectDebridConfigEncoder()
private val formatter = DebridStreamFormatter()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val mutex = Mutex()
private val streamCache = mutableMapOf<DirectDebridStreamCacheKey, CachedDirectDebridStreams>()
private val inFlightFetches = mutableMapOf<DirectDebridStreamCacheKey, Deferred<AddonStreamGroup>>()
fun configuredTargets(): List<DirectDebridStreamTarget> {
DebridSettingsRepository.ensureLoaded()
@ -33,6 +48,12 @@ object DirectDebridStreamSource {
}
}
fun sourceNames(): List<String> =
configuredTargets().map { it.addonName }
fun isEnabled(): Boolean =
sourceNames().isNotEmpty()
fun placeholders(): List<AddonStreamGroup> =
configuredTargets().map { target ->
AddonStreamGroup(
@ -43,6 +64,36 @@ object DirectDebridStreamSource {
)
}
fun preloadStreams(type: String, videoId: String) {
if (type.isBlank() || videoId.isBlank()) return
configuredTargets().forEach { target ->
scope.launch {
runCatching { fetchProviderStreams(type, videoId, target) }
}
}
}
suspend fun fetchStreams(type: String, videoId: String): DirectDebridStreamFetchResult {
val targets = configuredTargets()
if (targets.isEmpty()) return DirectDebridStreamFetchResult.Disabled
val results = mutableListOf<AddonStreamGroup>()
val errors = mutableListOf<String>()
targets.forEach { target ->
val group = fetchProviderStreams(type, videoId, target)
when {
group.streams.isNotEmpty() -> results += group
!group.error.isNullOrBlank() -> errors += group.error
}
}
return when {
results.isNotEmpty() -> DirectDebridStreamFetchResult.Success(results)
errors.isNotEmpty() -> DirectDebridStreamFetchResult.Error(errors.first())
else -> DirectDebridStreamFetchResult.Empty
}
}
suspend fun fetchProviderStreams(
type: String,
videoId: String,
@ -54,6 +105,89 @@ object DirectDebridStreamSource {
return target.emptyGroup()
}
val cacheKey = DirectDebridStreamCacheKey(
providerId = target.provider.id,
type = type.trim().lowercase(),
videoId = videoId.trim(),
baseUrl = baseUrl,
settingsFingerprint = settings.toString(),
)
cachedGroup(cacheKey)?.let { return it }
var ownsFetch = false
val newFetch = scope.async(start = CoroutineStart.LAZY) {
fetchProviderStreamsUncached(
baseUrl = baseUrl,
type = type,
videoId = videoId,
target = target,
settings = settings,
)
}
val activeFetch = mutex.withLock {
cachedGroupLocked(cacheKey)?.let { cached ->
return@withLock null to cached
}
val existing = inFlightFetches[cacheKey]
if (existing != null) {
existing to null
} else {
inFlightFetches[cacheKey] = newFetch
ownsFetch = true
newFetch to null
}
}
activeFetch.second?.let {
newFetch.cancel()
return it
}
val deferred = activeFetch.first ?: return target.errorGroup("Could not start Direct Debrid fetch")
if (!ownsFetch) newFetch.cancel()
if (ownsFetch) deferred.start()
return try {
val result = deferred.await()
if (ownsFetch && result.streams.isNotEmpty() && result.error == null) {
mutex.withLock {
streamCache[cacheKey] = CachedDirectDebridStreams(
group = result,
createdAtMs = epochMs(),
)
}
}
result
} finally {
if (ownsFetch) {
mutex.withLock {
if (inFlightFetches[cacheKey] === deferred) {
inFlightFetches.remove(cacheKey)
}
}
}
}
}
private suspend fun cachedGroup(cacheKey: DirectDebridStreamCacheKey): AddonStreamGroup? =
mutex.withLock { cachedGroupLocked(cacheKey) }
private fun cachedGroupLocked(cacheKey: DirectDebridStreamCacheKey): AddonStreamGroup? {
val cached = streamCache[cacheKey] ?: return null
val age = epochMs() - cached.createdAtMs
return if (age in 0..STREAM_CACHE_TTL_MS) {
cached.group
} else {
streamCache.remove(cacheKey)
null
}
}
private suspend fun fetchProviderStreamsUncached(
baseUrl: String,
type: String,
videoId: String,
target: DirectDebridStreamTarget,
settings: DebridSettings,
): AddonStreamGroup {
val credential = DebridServiceCredential(target.provider, target.apiKey)
val url = "$baseUrl/${encoder.encode(credential)}/client-stream/${encodePathSegment(type)}/${encodePathSegment(videoId)}.json"
return try {
@ -63,7 +197,7 @@ object DirectDebridStreamSource {
addonName = DirectDebridStreamFilter.FALLBACK_SOURCE_NAME,
addonId = target.addonId,
)
.let(DirectDebridStreamFilter::filterInstant)
.let { DirectDebridStreamFilter.filterInstant(it, settings) }
.filter { stream -> stream.clientResolve?.service.equals(target.provider.id, ignoreCase = true) }
.map { stream -> formatter.format(stream.copy(addonId = target.addonId), settings) }
@ -76,13 +210,7 @@ object DirectDebridStreamSource {
} catch (error: Exception) {
if (error is CancellationException) throw error
log.w(error) { "Direct debrid ${target.provider.id} stream fetch failed" }
AddonStreamGroup(
addonName = target.addonName,
addonId = target.addonId,
streams = emptyList(),
isLoading = false,
error = error.message,
)
target.errorGroup(error.message)
}
}
@ -93,4 +221,33 @@ object DirectDebridStreamSource {
streams = emptyList(),
isLoading = false,
)
private fun DirectDebridStreamTarget.errorGroup(message: String?): AddonStreamGroup =
AddonStreamGroup(
addonName = addonName,
addonId = addonId,
streams = emptyList(),
isLoading = false,
error = message,
)
}
private data class DirectDebridStreamCacheKey(
val providerId: String,
val type: String,
val videoId: String,
val baseUrl: String,
val settingsFingerprint: String,
)
private data class CachedDirectDebridStreams(
val group: AddonStreamGroup,
val createdAtMs: Long,
)
sealed class DirectDebridStreamFetchResult {
data object Disabled : DirectDebridStreamFetchResult()
data object Empty : DirectDebridStreamFetchResult()
data class Success(val streams: List<AddonStreamGroup>) : DirectDebridStreamFetchResult()
data class Error(val message: String) : DirectDebridStreamFetchResult()
}

View file

@ -1,6 +1,9 @@
package com.nuvio.app.features.debrid
import com.nuvio.app.features.streams.StreamClientResolve
import com.nuvio.app.features.streams.StreamClientResolveParsed
import com.nuvio.app.features.streams.StreamClientResolveRaw
import com.nuvio.app.features.streams.StreamClientResolveStream
import com.nuvio.app.features.streams.StreamItem
import kotlin.test.Test
import kotlin.test.assertEquals
@ -48,25 +51,160 @@ class DirectDebridStreamFilterTest {
assertTrue(plainTorrent.isTorrentStream)
}
@Test
fun `sorts and limits streams by quality and size`() {
val streams = listOf(
stream(resolution = "1080p", size = 20),
stream(resolution = "2160p", size = 10),
stream(resolution = "2160p", size = 30),
stream(resolution = "720p", size = 40),
)
val filtered = DirectDebridStreamFilter.filterInstant(
streams,
DebridSettings(
streamMaxResults = 2,
streamSortMode = DebridStreamSortMode.QUALITY_DESC,
),
)
assertEquals(listOf(30L, 10L), filtered.map { it.clientResolve?.stream?.raw?.size })
}
@Test
fun `filters minimum quality dolby vision hdr and codec`() {
val hdrHevc = stream(resolution = "2160p", hdr = listOf("HDR10"), codec = "HEVC", size = 10)
val dvHevc = stream(resolution = "2160p", hdr = listOf("DV", "HDR10"), codec = "HEVC", size = 20)
val sdrAvc = stream(resolution = "1080p", codec = "AVC", size = 30)
val hdHevc = stream(resolution = "720p", codec = "HEVC", size = 40)
val noDvHdrHevc4k = DirectDebridStreamFilter.filterInstant(
listOf(hdrHevc, dvHevc, sdrAvc, hdHevc),
DebridSettings(
streamMinimumQuality = DebridStreamMinimumQuality.P2160,
streamDolbyVisionFilter = DebridStreamFeatureFilter.EXCLUDE,
streamHdrFilter = DebridStreamFeatureFilter.ONLY,
streamCodecFilter = DebridStreamCodecFilter.HEVC,
),
)
assertEquals(listOf(10L), noDvHdrHevc4k.map { it.clientResolve?.stream?.raw?.size })
val dvOnly = DirectDebridStreamFilter.filterInstant(
listOf(hdrHevc, dvHevc, sdrAvc, hdHevc),
DebridSettings(streamDolbyVisionFilter = DebridStreamFeatureFilter.ONLY),
)
assertEquals(listOf(20L), dvOnly.map { it.clientResolve?.stream?.raw?.size })
}
@Test
fun `applies stream preference filters and sort criteria`() {
val remuxAtmos = stream(
resolution = "2160p",
quality = "BluRay REMUX",
codec = "HEVC",
audio = listOf("Atmos", "TrueHD"),
channels = listOf("7.1"),
languages = listOf("en"),
group = "GOOD",
size = 40_000_000_000,
)
val webAac = stream(
resolution = "2160p",
quality = "WEB-DL",
codec = "AVC",
audio = listOf("AAC"),
channels = listOf("2.0"),
languages = listOf("en"),
group = "NOPE",
size = 4_000_000_000,
)
val blurayDts = stream(
resolution = "1080p",
quality = "BluRay",
codec = "AVC",
audio = listOf("DTS"),
channels = listOf("5.1"),
languages = listOf("hi"),
group = "GOOD",
size = 12_000_000_000,
)
val filtered = DirectDebridStreamFilter.filterInstant(
listOf(webAac, blurayDts, remuxAtmos),
DebridSettings(
streamPreferences = DebridStreamPreferences(
maxResults = 2,
maxPerResolution = 1,
sizeMinGb = 5,
requiredResolutions = listOf(DebridStreamResolution.P2160, DebridStreamResolution.P1080),
excludedQualities = listOf(DebridStreamQuality.WEB_DL),
requiredAudioChannels = listOf(DebridStreamAudioChannel.CH_7_1, DebridStreamAudioChannel.CH_5_1),
excludedEncodes = listOf(DebridStreamEncode.UNKNOWN),
excludedLanguages = listOf(DebridStreamLanguage.IT),
requiredReleaseGroups = listOf("GOOD"),
sortCriteria = listOf(
DebridStreamSortCriterion(DebridStreamSortKey.AUDIO_TAG, DebridStreamSortDirection.DESC),
DebridStreamSortCriterion(DebridStreamSortKey.SIZE, DebridStreamSortDirection.ASC),
),
),
),
)
assertEquals(listOf(40_000_000_000L, 12_000_000_000L), filtered.map { it.clientResolve?.stream?.raw?.size })
}
private fun stream(
service: String?,
cached: Boolean?,
service: String? = DebridProviders.TORBOX_ID,
cached: Boolean? = true,
type: String = "debrid",
infoHash: String = "hash",
fileIdx: Int = 1,
resolution: String? = null,
quality: String? = null,
hdr: List<String> = emptyList(),
codec: String? = null,
audio: List<String> = emptyList(),
channels: List<String> = emptyList(),
languages: List<String> = emptyList(),
group: String? = null,
size: Long? = null,
): StreamItem =
StreamItem(
name = "Stream",
name = "Stream ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}",
description = "Stream ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}",
addonName = "Direct Debrid",
addonId = "debrid",
clientResolve = StreamClientResolve(
type = type,
service = service,
isCached = cached,
infoHash = infoHash,
infoHash = infoHash + size.orEmptyHashPart() + resolution.orEmpty() + quality.orEmpty() + codec.orEmpty(),
fileIdx = fileIdx,
filename = "video.mkv",
filename = "video ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}.mkv",
torrentName = "Torrent ${resolution.orEmpty()} ${quality.orEmpty()}",
stream = StreamClientResolveStream(
raw = StreamClientResolveRaw(
torrentName = "Torrent ${resolution.orEmpty()} ${quality.orEmpty()}",
filename = "video ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}.mkv",
size = size,
folderSize = size,
parsed = StreamClientResolveParsed(
resolution = resolution,
quality = quality,
hdr = hdr,
codec = codec,
audio = audio,
channels = channels,
languages = languages,
group = group,
),
),
),
),
)
}
private fun Long?.orEmptyHashPart(): String =
this?.toString().orEmpty()

View file

@ -17,6 +17,13 @@ actual object DebridSettingsStorage {
private const val torboxApiKeyKey = "debrid_torbox_api_key"
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
private const val instantPlaybackPreparationLimitKey = "debrid_instant_playback_preparation_limit"
private const val streamMaxResultsKey = "debrid_stream_max_results"
private const val streamSortModeKey = "debrid_stream_sort_mode"
private const val streamMinimumQualityKey = "debrid_stream_minimum_quality"
private const val streamDolbyVisionFilterKey = "debrid_stream_dolby_vision_filter"
private const val streamHdrFilterKey = "debrid_stream_hdr_filter"
private const val streamCodecFilterKey = "debrid_stream_codec_filter"
private const val streamPreferencesKey = "debrid_stream_preferences"
private const val streamNameTemplateKey = "debrid_stream_name_template"
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
private val syncKeys = listOf(
@ -24,6 +31,13 @@ actual object DebridSettingsStorage {
torboxApiKeyKey,
realDebridApiKeyKey,
instantPlaybackPreparationLimitKey,
streamMaxResultsKey,
streamSortModeKey,
streamMinimumQualityKey,
streamDolbyVisionFilterKey,
streamHdrFilterKey,
streamCodecFilterKey,
streamPreferencesKey,
streamNameTemplateKey,
streamDescriptionTemplateKey,
)
@ -52,6 +66,48 @@ actual object DebridSettingsStorage {
saveInt(instantPlaybackPreparationLimitKey, limit)
}
actual fun loadStreamMaxResults(): Int? = loadInt(streamMaxResultsKey)
actual fun saveStreamMaxResults(maxResults: Int) {
saveInt(streamMaxResultsKey, maxResults)
}
actual fun loadStreamSortMode(): String? = loadString(streamSortModeKey)
actual fun saveStreamSortMode(mode: String) {
saveString(streamSortModeKey, mode)
}
actual fun loadStreamMinimumQuality(): String? = loadString(streamMinimumQualityKey)
actual fun saveStreamMinimumQuality(quality: String) {
saveString(streamMinimumQualityKey, quality)
}
actual fun loadStreamDolbyVisionFilter(): String? = loadString(streamDolbyVisionFilterKey)
actual fun saveStreamDolbyVisionFilter(filter: String) {
saveString(streamDolbyVisionFilterKey, filter)
}
actual fun loadStreamHdrFilter(): String? = loadString(streamHdrFilterKey)
actual fun saveStreamHdrFilter(filter: String) {
saveString(streamHdrFilterKey, filter)
}
actual fun loadStreamCodecFilter(): String? = loadString(streamCodecFilterKey)
actual fun saveStreamCodecFilter(filter: String) {
saveString(streamCodecFilterKey, filter)
}
actual fun loadStreamPreferences(): String? = loadString(streamPreferencesKey)
actual fun saveStreamPreferences(preferences: String) {
saveString(streamPreferencesKey, preferences)
}
actual fun loadStreamNameTemplate(): String? = loadString(streamNameTemplateKey)
actual fun saveStreamNameTemplate(template: String) {
@ -104,6 +160,13 @@ actual object DebridSettingsStorage {
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(it)) }
loadStreamMaxResults()?.let { put(streamMaxResultsKey, encodeSyncInt(it)) }
loadStreamSortMode()?.let { put(streamSortModeKey, encodeSyncString(it)) }
loadStreamMinimumQuality()?.let { put(streamMinimumQualityKey, encodeSyncString(it)) }
loadStreamDolbyVisionFilter()?.let { put(streamDolbyVisionFilterKey, encodeSyncString(it)) }
loadStreamHdrFilter()?.let { put(streamHdrFilterKey, encodeSyncString(it)) }
loadStreamCodecFilter()?.let { put(streamCodecFilterKey, encodeSyncString(it)) }
loadStreamPreferences()?.let { put(streamPreferencesKey, encodeSyncString(it)) }
loadStreamNameTemplate()?.let { put(streamNameTemplateKey, encodeSyncString(it)) }
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
}
@ -117,6 +180,13 @@ actual object DebridSettingsStorage {
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
payload.decodeSyncInt(streamMaxResultsKey)?.let(::saveStreamMaxResults)
payload.decodeSyncString(streamSortModeKey)?.let(::saveStreamSortMode)
payload.decodeSyncString(streamMinimumQualityKey)?.let(::saveStreamMinimumQuality)
payload.decodeSyncString(streamDolbyVisionFilterKey)?.let(::saveStreamDolbyVisionFilter)
payload.decodeSyncString(streamHdrFilterKey)?.let(::saveStreamHdrFilter)
payload.decodeSyncString(streamCodecFilterKey)?.let(::saveStreamCodecFilter)
payload.decodeSyncString(streamPreferencesKey)?.let(::saveStreamPreferences)
payload.decodeSyncString(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
}