mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 01:32:08 +00:00
feat(debrid): sorting and filtering
This commit is contained in:
parent
5aee64e25e
commit
3e40e47b78
10 changed files with 2232 additions and 164 deletions
|
|
@ -19,6 +19,13 @@ actual object DebridSettingsStorage {
|
||||||
private const val torboxApiKeyKey = "debrid_torbox_api_key"
|
private const val torboxApiKeyKey = "debrid_torbox_api_key"
|
||||||
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
|
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
|
||||||
private const val instantPlaybackPreparationLimitKey = "debrid_instant_playback_preparation_limit"
|
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 streamNameTemplateKey = "debrid_stream_name_template"
|
||||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||||
private val syncKeys = listOf(
|
private val syncKeys = listOf(
|
||||||
|
|
@ -26,6 +33,13 @@ actual object DebridSettingsStorage {
|
||||||
torboxApiKeyKey,
|
torboxApiKeyKey,
|
||||||
realDebridApiKeyKey,
|
realDebridApiKeyKey,
|
||||||
instantPlaybackPreparationLimitKey,
|
instantPlaybackPreparationLimitKey,
|
||||||
|
streamMaxResultsKey,
|
||||||
|
streamSortModeKey,
|
||||||
|
streamMinimumQualityKey,
|
||||||
|
streamDolbyVisionFilterKey,
|
||||||
|
streamHdrFilterKey,
|
||||||
|
streamCodecFilterKey,
|
||||||
|
streamPreferencesKey,
|
||||||
streamNameTemplateKey,
|
streamNameTemplateKey,
|
||||||
streamDescriptionTemplateKey,
|
streamDescriptionTemplateKey,
|
||||||
)
|
)
|
||||||
|
|
@ -60,6 +74,48 @@ actual object DebridSettingsStorage {
|
||||||
saveInt(instantPlaybackPreparationLimitKey, limit)
|
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 loadStreamNameTemplate(): String? = loadString(streamNameTemplateKey)
|
||||||
|
|
||||||
actual fun saveStreamNameTemplate(template: String) {
|
actual fun saveStreamNameTemplate(template: String) {
|
||||||
|
|
@ -121,6 +177,13 @@ actual object DebridSettingsStorage {
|
||||||
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
||||||
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
||||||
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(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)) }
|
loadStreamNameTemplate()?.let { put(streamNameTemplateKey, encodeSyncString(it)) }
|
||||||
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
|
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +197,13 @@ actual object DebridSettingsStorage {
|
||||||
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
||||||
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
||||||
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
|
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(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
|
||||||
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
<string name="action_resume">Resume</string>
|
<string name="action_resume">Resume</string>
|
||||||
<string name="action_retry">Retry</string>
|
<string name="action_retry">Retry</string>
|
||||||
<string name="action_save">Save</string>
|
<string name="action_save">Save</string>
|
||||||
|
<string name="action_saving">Saving…</string>
|
||||||
<string name="action_validate">Validate</string>
|
<string name="action_validate">Validate</string>
|
||||||
<string name="addon_installing">Installing</string>
|
<string name="addon_installing">Installing</string>
|
||||||
<string name="addon_title">Addons</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_add_key_first">Add an API key first.</string>
|
||||||
<string name="settings_debrid_section_providers">Account</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_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_section_instant_playback">Instant Playback</string>
|
||||||
<string name="settings_debrid_prepare_instant_playback">Prepare links</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>
|
<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_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 template</string>
|
||||||
<string name="settings_debrid_description_template_description">Controls the metadata shown under each source.</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_valid">API key validated.</string>
|
||||||
<string name="settings_debrid_key_invalid">Could not validate this API key.</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>
|
<string name="settings_mdb_add_api_key_first">Add your MDBList API key below before turning ratings on.</string>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
package com.nuvio.app.features.debrid
|
package com.nuvio.app.features.debrid
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
data class DebridSettings(
|
data class DebridSettings(
|
||||||
val enabled: Boolean = false,
|
val enabled: Boolean = false,
|
||||||
val torboxApiKey: String = "",
|
val torboxApiKey: String = "",
|
||||||
val realDebridApiKey: String = "",
|
val realDebridApiKey: String = "",
|
||||||
val instantPlaybackPreparationLimit: Int = 0,
|
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 streamNameTemplate: String = DebridStreamFormatterDefaults.NAME_TEMPLATE,
|
||||||
val streamDescriptionTemplate: String = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
|
val streamDescriptionTemplate: String = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
|
||||||
) {
|
) {
|
||||||
|
|
@ -12,8 +21,236 @@ data class DebridSettings(
|
||||||
get() = DebridProviders.configuredServices(this).isNotEmpty()
|
get() = DebridProviders.configuredServices(this).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal const val DEBRID_PREPARE_INSTANT_PLAYBACK_DEFAULT_LIMIT = 2
|
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_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)
|
value.coerceIn(0, DEBRID_PREPARE_INSTANT_PLAYBACK_MAX_LIMIT)
|
||||||
|
|
||||||
|
fun normalizeDebridStreamMaxResults(value: Int): Int =
|
||||||
|
if (value <= 0) 0 else value.coerceIn(1, 100)
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,34 @@ package com.nuvio.app.features.debrid
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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 {
|
object DebridSettingsRepository {
|
||||||
private val _uiState = MutableStateFlow(DebridSettings())
|
private val _uiState = MutableStateFlow(DebridSettings())
|
||||||
val uiState: StateFlow<DebridSettings> = _uiState.asStateFlow()
|
val uiState: StateFlow<DebridSettings> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
|
||||||
private var hasLoaded = false
|
private var hasLoaded = false
|
||||||
private var enabled = false
|
private var enabled = false
|
||||||
private var torboxApiKey = ""
|
private var torboxApiKey = ""
|
||||||
private var realDebridApiKey = ""
|
private var realDebridApiKey = ""
|
||||||
private var instantPlaybackPreparationLimit = 0
|
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 streamNameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE
|
||||||
private var streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
|
private var streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
|
||||||
|
|
||||||
|
|
@ -68,6 +86,78 @@ object DebridSettingsRepository {
|
||||||
DebridSettingsStorage.saveInstantPlaybackPreparationLimit(normalized)
|
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) {
|
fun setStreamNameTemplate(value: String) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
val normalized = value.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
|
val normalized = value.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
|
||||||
|
|
@ -86,15 +176,22 @@ object DebridSettingsRepository {
|
||||||
DebridSettingsStorage.saveStreamDescriptionTemplate(normalized)
|
DebridSettingsStorage.saveStreamDescriptionTemplate(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetStreamTemplates() {
|
fun setStreamTemplates(nameTemplate: String, descriptionTemplate: String) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
streamNameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE
|
streamNameTemplate = nameTemplate.ifBlank { DebridStreamFormatterDefaults.NAME_TEMPLATE }
|
||||||
streamDescriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE
|
streamDescriptionTemplate = descriptionTemplate.ifBlank { DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE }
|
||||||
publish()
|
publish()
|
||||||
DebridSettingsStorage.saveStreamNameTemplate(streamNameTemplate)
|
DebridSettingsStorage.saveStreamNameTemplate(streamNameTemplate)
|
||||||
DebridSettingsStorage.saveStreamDescriptionTemplate(streamDescriptionTemplate)
|
DebridSettingsStorage.saveStreamDescriptionTemplate(streamDescriptionTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetStreamTemplates() {
|
||||||
|
setStreamTemplates(
|
||||||
|
nameTemplate = DebridStreamFormatterDefaults.NAME_TEMPLATE,
|
||||||
|
descriptionTemplate = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun disableIfNoKeys() {
|
private fun disableIfNoKeys() {
|
||||||
if (!hasVisibleApiKey()) {
|
if (!hasVisibleApiKey()) {
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
@ -114,6 +211,36 @@ object DebridSettingsRepository {
|
||||||
instantPlaybackPreparationLimit = normalizeDebridInstantPlaybackPreparationLimit(
|
instantPlaybackPreparationLimit = normalizeDebridInstantPlaybackPreparationLimit(
|
||||||
DebridSettingsStorage.loadInstantPlaybackPreparationLimit() ?: 0,
|
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()
|
streamNameTemplate = DebridSettingsStorage.loadStreamNameTemplate()
|
||||||
?.takeIf { it.isNotBlank() }
|
?.takeIf { it.isNotBlank() }
|
||||||
?: DebridStreamFormatterDefaults.NAME_TEMPLATE
|
?: DebridStreamFormatterDefaults.NAME_TEMPLATE
|
||||||
|
|
@ -129,8 +256,164 @@ object DebridSettingsRepository {
|
||||||
torboxApiKey = torboxApiKey,
|
torboxApiKey = torboxApiKey,
|
||||||
realDebridApiKey = realDebridApiKey,
|
realDebridApiKey = realDebridApiKey,
|
||||||
instantPlaybackPreparationLimit = instantPlaybackPreparationLimit,
|
instantPlaybackPreparationLimit = instantPlaybackPreparationLimit,
|
||||||
|
streamMaxResults = streamMaxResults,
|
||||||
|
streamSortMode = streamSortMode,
|
||||||
|
streamMinimumQuality = streamMinimumQuality,
|
||||||
|
streamDolbyVisionFilter = streamDolbyVisionFilter,
|
||||||
|
streamHdrFilter = streamHdrFilter,
|
||||||
|
streamCodecFilter = streamCodecFilter,
|
||||||
|
streamPreferences = streamPreferences,
|
||||||
streamNameTemplate = streamNameTemplate,
|
streamNameTemplate = streamNameTemplate,
|
||||||
streamDescriptionTemplate = streamDescriptionTemplate,
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,20 @@ internal expect object DebridSettingsStorage {
|
||||||
fun saveRealDebridApiKey(apiKey: String)
|
fun saveRealDebridApiKey(apiKey: String)
|
||||||
fun loadInstantPlaybackPreparationLimit(): Int?
|
fun loadInstantPlaybackPreparationLimit(): Int?
|
||||||
fun saveInstantPlaybackPreparationLimit(limit: 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 loadStreamNameTemplate(): String?
|
||||||
fun saveStreamNameTemplate(template: String)
|
fun saveStreamNameTemplate(template: String)
|
||||||
fun loadStreamDescriptionTemplate(): String?
|
fun loadStreamDescriptionTemplate(): String?
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import com.nuvio.app.features.streams.StreamItem
|
||||||
object DirectDebridStreamFilter {
|
object DirectDebridStreamFilter {
|
||||||
const val FALLBACK_SOURCE_NAME = "Direct Debrid"
|
const val FALLBACK_SOURCE_NAME = "Direct Debrid"
|
||||||
|
|
||||||
fun filterInstant(streams: List<StreamItem>): List<StreamItem> =
|
fun filterInstant(streams: List<StreamItem>, settings: DebridSettings? = null): List<StreamItem> {
|
||||||
streams
|
val instantStreams = streams
|
||||||
.filter(::isInstantCandidate)
|
.filter(::isInstantCandidate)
|
||||||
.map { stream ->
|
.map { stream ->
|
||||||
val providerId = stream.clientResolve?.service
|
val providerId = stream.clientResolve?.service
|
||||||
|
|
@ -27,6 +27,8 @@ object DirectDebridStreamFilter {
|
||||||
stream.title,
|
stream.title,
|
||||||
).joinToString("|")
|
).joinToString("|")
|
||||||
}
|
}
|
||||||
|
return if (settings == null) instantStreams else applyPreferences(instantStreams, settings)
|
||||||
|
}
|
||||||
|
|
||||||
fun isInstantCandidate(stream: StreamItem): Boolean {
|
fun isInstantCandidate(stream: StreamItem): Boolean {
|
||||||
val resolve = stream.clientResolve ?: return false
|
val resolve = stream.clientResolve ?: return false
|
||||||
|
|
@ -37,5 +39,387 @@ object DirectDebridStreamFilter {
|
||||||
|
|
||||||
fun isDirectDebridSourceName(addonName: String): Boolean =
|
fun isDirectDebridSourceName(addonName: String): Boolean =
|
||||||
DebridProviders.all().any { addonName == DebridProviders.instantName(it.id) }
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,20 @@ import co.touchlab.kermit.Logger
|
||||||
import com.nuvio.app.features.addons.httpGetText
|
import com.nuvio.app.features.addons.httpGetText
|
||||||
import com.nuvio.app.features.streams.AddonStreamGroup
|
import com.nuvio.app.features.streams.AddonStreamGroup
|
||||||
import com.nuvio.app.features.streams.StreamParser
|
import com.nuvio.app.features.streams.StreamParser
|
||||||
|
import com.nuvio.app.features.streams.epochMs
|
||||||
import kotlinx.coroutines.CancellationException
|
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 DIRECT_DEBRID_TAG = "DirectDebridStreams"
|
||||||
|
private const val STREAM_CACHE_TTL_MS = 5L * 60L * 1000L
|
||||||
|
|
||||||
data class DirectDebridStreamTarget(
|
data class DirectDebridStreamTarget(
|
||||||
val provider: DebridProvider,
|
val provider: DebridProvider,
|
||||||
|
|
@ -20,6 +31,10 @@ object DirectDebridStreamSource {
|
||||||
private val log = Logger.withTag(DIRECT_DEBRID_TAG)
|
private val log = Logger.withTag(DIRECT_DEBRID_TAG)
|
||||||
private val encoder = DirectDebridConfigEncoder()
|
private val encoder = DirectDebridConfigEncoder()
|
||||||
private val formatter = DebridStreamFormatter()
|
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> {
|
fun configuredTargets(): List<DirectDebridStreamTarget> {
|
||||||
DebridSettingsRepository.ensureLoaded()
|
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> =
|
fun placeholders(): List<AddonStreamGroup> =
|
||||||
configuredTargets().map { target ->
|
configuredTargets().map { target ->
|
||||||
AddonStreamGroup(
|
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(
|
suspend fun fetchProviderStreams(
|
||||||
type: String,
|
type: String,
|
||||||
videoId: String,
|
videoId: String,
|
||||||
|
|
@ -54,6 +105,89 @@ object DirectDebridStreamSource {
|
||||||
return target.emptyGroup()
|
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 credential = DebridServiceCredential(target.provider, target.apiKey)
|
||||||
val url = "$baseUrl/${encoder.encode(credential)}/client-stream/${encodePathSegment(type)}/${encodePathSegment(videoId)}.json"
|
val url = "$baseUrl/${encoder.encode(credential)}/client-stream/${encodePathSegment(type)}/${encodePathSegment(videoId)}.json"
|
||||||
return try {
|
return try {
|
||||||
|
|
@ -63,7 +197,7 @@ object DirectDebridStreamSource {
|
||||||
addonName = DirectDebridStreamFilter.FALLBACK_SOURCE_NAME,
|
addonName = DirectDebridStreamFilter.FALLBACK_SOURCE_NAME,
|
||||||
addonId = target.addonId,
|
addonId = target.addonId,
|
||||||
)
|
)
|
||||||
.let(DirectDebridStreamFilter::filterInstant)
|
.let { DirectDebridStreamFilter.filterInstant(it, settings) }
|
||||||
.filter { stream -> stream.clientResolve?.service.equals(target.provider.id, ignoreCase = true) }
|
.filter { stream -> stream.clientResolve?.service.equals(target.provider.id, ignoreCase = true) }
|
||||||
.map { stream -> formatter.format(stream.copy(addonId = target.addonId), settings) }
|
.map { stream -> formatter.format(stream.copy(addonId = target.addonId), settings) }
|
||||||
|
|
||||||
|
|
@ -76,13 +210,7 @@ object DirectDebridStreamSource {
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
if (error is CancellationException) throw error
|
if (error is CancellationException) throw error
|
||||||
log.w(error) { "Direct debrid ${target.provider.id} stream fetch failed" }
|
log.w(error) { "Direct debrid ${target.provider.id} stream fetch failed" }
|
||||||
AddonStreamGroup(
|
target.errorGroup(error.message)
|
||||||
addonName = target.addonName,
|
|
||||||
addonId = target.addonId,
|
|
||||||
streams = emptyList(),
|
|
||||||
isLoading = false,
|
|
||||||
error = error.message,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,4 +221,33 @@ object DirectDebridStreamSource {
|
||||||
streams = emptyList(),
|
streams = emptyList(),
|
||||||
isLoading = false,
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,9 @@
|
||||||
package com.nuvio.app.features.debrid
|
package com.nuvio.app.features.debrid
|
||||||
|
|
||||||
import com.nuvio.app.features.streams.StreamClientResolve
|
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 com.nuvio.app.features.streams.StreamItem
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
@ -48,25 +51,160 @@ class DirectDebridStreamFilterTest {
|
||||||
assertTrue(plainTorrent.isTorrentStream)
|
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(
|
private fun stream(
|
||||||
service: String?,
|
service: String? = DebridProviders.TORBOX_ID,
|
||||||
cached: Boolean?,
|
cached: Boolean? = true,
|
||||||
type: String = "debrid",
|
type: String = "debrid",
|
||||||
infoHash: String = "hash",
|
infoHash: String = "hash",
|
||||||
fileIdx: Int = 1,
|
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 =
|
||||||
StreamItem(
|
StreamItem(
|
||||||
name = "Stream",
|
name = "Stream ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}",
|
||||||
|
description = "Stream ${resolution.orEmpty()} ${quality.orEmpty()} ${codec.orEmpty()}",
|
||||||
addonName = "Direct Debrid",
|
addonName = "Direct Debrid",
|
||||||
addonId = "debrid",
|
addonId = "debrid",
|
||||||
clientResolve = StreamClientResolve(
|
clientResolve = StreamClientResolve(
|
||||||
type = type,
|
type = type,
|
||||||
service = service,
|
service = service,
|
||||||
isCached = cached,
|
isCached = cached,
|
||||||
infoHash = infoHash,
|
infoHash = infoHash + size.orEmptyHashPart() + resolution.orEmpty() + quality.orEmpty() + codec.orEmpty(),
|
||||||
fileIdx = fileIdx,
|
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()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,13 @@ actual object DebridSettingsStorage {
|
||||||
private const val torboxApiKeyKey = "debrid_torbox_api_key"
|
private const val torboxApiKeyKey = "debrid_torbox_api_key"
|
||||||
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
|
private const val realDebridApiKeyKey = "debrid_real_debrid_api_key"
|
||||||
private const val instantPlaybackPreparationLimitKey = "debrid_instant_playback_preparation_limit"
|
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 streamNameTemplateKey = "debrid_stream_name_template"
|
||||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||||
private val syncKeys = listOf(
|
private val syncKeys = listOf(
|
||||||
|
|
@ -24,6 +31,13 @@ actual object DebridSettingsStorage {
|
||||||
torboxApiKeyKey,
|
torboxApiKeyKey,
|
||||||
realDebridApiKeyKey,
|
realDebridApiKeyKey,
|
||||||
instantPlaybackPreparationLimitKey,
|
instantPlaybackPreparationLimitKey,
|
||||||
|
streamMaxResultsKey,
|
||||||
|
streamSortModeKey,
|
||||||
|
streamMinimumQualityKey,
|
||||||
|
streamDolbyVisionFilterKey,
|
||||||
|
streamHdrFilterKey,
|
||||||
|
streamCodecFilterKey,
|
||||||
|
streamPreferencesKey,
|
||||||
streamNameTemplateKey,
|
streamNameTemplateKey,
|
||||||
streamDescriptionTemplateKey,
|
streamDescriptionTemplateKey,
|
||||||
)
|
)
|
||||||
|
|
@ -52,6 +66,48 @@ actual object DebridSettingsStorage {
|
||||||
saveInt(instantPlaybackPreparationLimitKey, limit)
|
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 loadStreamNameTemplate(): String? = loadString(streamNameTemplateKey)
|
||||||
|
|
||||||
actual fun saveStreamNameTemplate(template: String) {
|
actual fun saveStreamNameTemplate(template: String) {
|
||||||
|
|
@ -104,6 +160,13 @@ actual object DebridSettingsStorage {
|
||||||
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
||||||
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
||||||
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(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)) }
|
loadStreamNameTemplate()?.let { put(streamNameTemplateKey, encodeSyncString(it)) }
|
||||||
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
|
loadStreamDescriptionTemplate()?.let { put(streamDescriptionTemplateKey, encodeSyncString(it)) }
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +180,13 @@ actual object DebridSettingsStorage {
|
||||||
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
||||||
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
||||||
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
|
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(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
|
||||||
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue