mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-22 17:52:06 +00:00
feat(debrid): neutralize architecture
This commit is contained in:
parent
782f65aaff
commit
075c5f8f51
23 changed files with 594 additions and 371 deletions
|
|
@ -28,21 +28,20 @@ actual object DebridSettingsStorage {
|
|||
private const val streamPreferencesKey = "debrid_stream_preferences"
|
||||
private const val streamNameTemplateKey = "debrid_stream_name_template"
|
||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||
private val syncKeys = listOf(
|
||||
enabledKey,
|
||||
torboxApiKeyKey,
|
||||
realDebridApiKeyKey,
|
||||
instantPlaybackPreparationLimitKey,
|
||||
streamMaxResultsKey,
|
||||
streamSortModeKey,
|
||||
streamMinimumQualityKey,
|
||||
streamDolbyVisionFilterKey,
|
||||
streamHdrFilterKey,
|
||||
streamCodecFilterKey,
|
||||
streamPreferencesKey,
|
||||
streamNameTemplateKey,
|
||||
streamDescriptionTemplateKey,
|
||||
)
|
||||
private fun syncKeys(): List<String> =
|
||||
listOf(
|
||||
enabledKey,
|
||||
instantPlaybackPreparationLimitKey,
|
||||
streamMaxResultsKey,
|
||||
streamSortModeKey,
|
||||
streamMinimumQualityKey,
|
||||
streamDolbyVisionFilterKey,
|
||||
streamHdrFilterKey,
|
||||
streamCodecFilterKey,
|
||||
streamPreferencesKey,
|
||||
streamNameTemplateKey,
|
||||
streamDescriptionTemplateKey,
|
||||
) + DebridProviders.all().map { providerApiKeyKey(it.id) }
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
|
|
@ -56,16 +55,23 @@ actual object DebridSettingsStorage {
|
|||
saveBoolean(enabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadTorboxApiKey(): String? = loadString(torboxApiKeyKey)
|
||||
actual fun loadProviderApiKey(providerId: String): String? =
|
||||
loadString(providerApiKeyKey(providerId))
|
||||
|
||||
actual fun saveTorboxApiKey(apiKey: String) {
|
||||
saveString(torboxApiKeyKey, apiKey)
|
||||
actual fun saveProviderApiKey(providerId: String, apiKey: String) {
|
||||
saveString(providerApiKeyKey(providerId), apiKey)
|
||||
}
|
||||
|
||||
actual fun loadRealDebridApiKey(): String? = loadString(realDebridApiKeyKey)
|
||||
actual fun loadTorboxApiKey(): String? = loadProviderApiKey(DebridProviders.TORBOX_ID)
|
||||
|
||||
actual fun saveTorboxApiKey(apiKey: String) {
|
||||
saveProviderApiKey(DebridProviders.TORBOX_ID, apiKey)
|
||||
}
|
||||
|
||||
actual fun loadRealDebridApiKey(): String? = loadProviderApiKey(DebridProviders.REAL_DEBRID_ID)
|
||||
|
||||
actual fun saveRealDebridApiKey(apiKey: String) {
|
||||
saveString(realDebridApiKeyKey, apiKey)
|
||||
saveProviderApiKey(DebridProviders.REAL_DEBRID_ID, apiKey)
|
||||
}
|
||||
|
||||
actual fun loadInstantPlaybackPreparationLimit(): Int? = loadInt(instantPlaybackPreparationLimitKey)
|
||||
|
|
@ -174,8 +180,11 @@ actual object DebridSettingsStorage {
|
|||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
||||
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
||||
DebridProviders.all().forEach { provider ->
|
||||
loadProviderApiKey(provider.id)?.let {
|
||||
put(providerApiKeyKey(provider.id), encodeSyncString(it))
|
||||
}
|
||||
}
|
||||
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(it)) }
|
||||
loadStreamMaxResults()?.let { put(streamMaxResultsKey, encodeSyncInt(it)) }
|
||||
loadStreamSortMode()?.let { put(streamSortModeKey, encodeSyncString(it)) }
|
||||
|
|
@ -190,12 +199,15 @@ actual object DebridSettingsStorage {
|
|||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
preferences?.edit()?.apply {
|
||||
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
|
||||
syncKeys().forEach { remove(ProfileScopedKey.of(it)) }
|
||||
}?.apply()
|
||||
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
||||
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
||||
DebridProviders.all().forEach { provider ->
|
||||
payload.decodeSyncString(providerApiKeyKey(provider.id))?.let { apiKey ->
|
||||
saveProviderApiKey(provider.id, apiKey)
|
||||
}
|
||||
}
|
||||
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
|
||||
payload.decodeSyncInt(streamMaxResultsKey)?.let(::saveStreamMaxResults)
|
||||
payload.decodeSyncString(streamSortModeKey)?.let(::saveStreamSortMode)
|
||||
|
|
@ -207,4 +219,14 @@ actual object DebridSettingsStorage {
|
|||
payload.decodeSyncString(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
|
||||
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
||||
}
|
||||
|
||||
private fun providerApiKeyKey(providerId: String): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId.trim().lowercase().replace(Regex("[^a-z0-9_]+"), "_")
|
||||
return when (normalized) {
|
||||
DebridProviders.TORBOX_ID -> torboxApiKeyKey
|
||||
DebridProviders.REAL_DEBRID_ID -> realDebridApiKeyKey
|
||||
else -> "debrid_${normalized}_api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -595,7 +595,10 @@
|
|||
<string name="settings_debrid_enable_description">Vis spillbare resultater fra tilkoblede kontoer.</string>
|
||||
<string name="settings_debrid_add_key_first">Legg til en API-nøkkel først.</string>
|
||||
<string name="settings_debrid_section_providers">Konto</string>
|
||||
<string name="settings_debrid_provider_torbox_description">Koble til Torbox-kontoen din.</string>
|
||||
<string name="settings_debrid_provider_description">Koble til %1$s-kontoen din.</string>
|
||||
<string name="settings_debrid_dialog_title">%1$s API-nøkkel</string>
|
||||
<string name="settings_debrid_dialog_subtitle">Skriv inn API-nøkkelen din for %1$s.</string>
|
||||
<string name="settings_debrid_dialog_placeholder">Skriv inn %1$s API-nøkkel</string>
|
||||
<string name="settings_debrid_section_instant_playback">Umiddelbar avspilling</string>
|
||||
<string name="settings_debrid_prepare_instant_playback">Forbered lenker</string>
|
||||
<string name="settings_debrid_prepare_instant_playback_description">Løs første kilder før avspilling starter.</string>
|
||||
|
|
|
|||
|
|
@ -596,10 +596,10 @@
|
|||
<string name="settings_debrid_enable_description">Show playable results from connected accounts.</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_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_provider_description">Connect your %1$s account.</string>
|
||||
<string name="settings_debrid_dialog_title">%1$s API Key</string>
|
||||
<string name="settings_debrid_dialog_subtitle">Enter your %1$s API key.</string>
|
||||
<string name="settings_debrid_dialog_placeholder">Enter %1$s API key</string>
|
||||
<string name="settings_debrid_not_set">Not set</string>
|
||||
<string name="settings_debrid_section_instant_playback">Instant Playback</string>
|
||||
<string name="settings_debrid_prepare_instant_playback">Prepare links</string>
|
||||
|
|
|
|||
|
|
@ -242,11 +242,7 @@ object DebridCredentialValidator {
|
|||
suspend fun validateProvider(providerId: String, apiKey: String): Boolean {
|
||||
val normalized = apiKey.trim()
|
||||
if (normalized.isBlank()) return false
|
||||
return when (DebridProviders.byId(providerId)?.id) {
|
||||
DebridProviders.TORBOX_ID -> TorboxApiClient.validateApiKey(normalized)
|
||||
DebridProviders.REAL_DEBRID_ID -> RealDebridApiClient.validateApiKey(normalized)
|
||||
else -> false
|
||||
}
|
||||
return DebridProviderApis.apiFor(providerId)?.validateApiKey(normalized) == true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ data class DebridProvider(
|
|||
val displayName: String,
|
||||
val shortName: String,
|
||||
val visibleInUi: Boolean = true,
|
||||
val capabilities: Set<DebridProviderCapability> = emptySet(),
|
||||
)
|
||||
|
||||
data class DebridServiceCredential(
|
||||
|
|
@ -12,6 +13,12 @@ data class DebridServiceCredential(
|
|||
val apiKey: String,
|
||||
)
|
||||
|
||||
enum class DebridProviderCapability {
|
||||
ClientResolve,
|
||||
LocalTorrentCacheCheck,
|
||||
LocalTorrentResolve,
|
||||
}
|
||||
|
||||
object DebridProviders {
|
||||
const val TORBOX_ID = "torbox"
|
||||
const val REAL_DEBRID_ID = "realdebrid"
|
||||
|
|
@ -20,6 +27,11 @@ object DebridProviders {
|
|||
id = TORBOX_ID,
|
||||
displayName = "Torbox",
|
||||
shortName = "TB",
|
||||
capabilities = setOf(
|
||||
DebridProviderCapability.ClientResolve,
|
||||
DebridProviderCapability.LocalTorrentCacheCheck,
|
||||
DebridProviderCapability.LocalTorrentResolve,
|
||||
),
|
||||
)
|
||||
|
||||
val RealDebrid = DebridProvider(
|
||||
|
|
@ -27,6 +39,7 @@ object DebridProviders {
|
|||
displayName = "Real-Debrid",
|
||||
shortName = "RD",
|
||||
visibleInUi = false,
|
||||
capabilities = setOf(DebridProviderCapability.ClientResolve),
|
||||
)
|
||||
|
||||
private val registered = listOf(Torbox, RealDebrid)
|
||||
|
|
@ -56,13 +69,11 @@ object DebridProviders {
|
|||
byId(id)?.shortName ?: id?.trim()?.takeIf { it.isNotBlank() }?.uppercase().orEmpty()
|
||||
|
||||
fun configuredServices(settings: DebridSettings): List<DebridServiceCredential> =
|
||||
buildList {
|
||||
settings.torboxApiKey.trim().takeIf { Torbox.visibleInUi && it.isNotBlank() }?.let { apiKey ->
|
||||
add(DebridServiceCredential(Torbox, apiKey))
|
||||
}
|
||||
settings.realDebridApiKey.trim().takeIf { RealDebrid.visibleInUi && it.isNotBlank() }?.let { apiKey ->
|
||||
add(DebridServiceCredential(RealDebrid, apiKey))
|
||||
}
|
||||
registered.mapNotNull { provider ->
|
||||
settings.apiKeyFor(provider.id)
|
||||
.trim()
|
||||
.takeIf { provider.visibleInUi && it.isNotBlank() }
|
||||
?.let { apiKey -> DebridServiceCredential(provider, apiKey) }
|
||||
}
|
||||
|
||||
fun configuredSourceNames(settings: DebridSettings): List<String> =
|
||||
|
|
@ -81,3 +92,6 @@ object DebridProviders {
|
|||
.ifBlank { "Debrid" }
|
||||
}
|
||||
}
|
||||
|
||||
fun DebridProvider.supports(capability: DebridProviderCapability): Boolean =
|
||||
capability in capabilities
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import com.nuvio.app.features.streams.StreamClientResolve
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
internal interface DebridProviderApi {
|
||||
val provider: DebridProvider
|
||||
|
||||
suspend fun validateApiKey(apiKey: String): Boolean
|
||||
|
||||
suspend fun resolveClientStream(
|
||||
stream: StreamItem,
|
||||
apiKey: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): DirectDebridResolveResult
|
||||
}
|
||||
|
||||
internal object DebridProviderApis {
|
||||
private val registered = listOf(
|
||||
TorboxDebridProviderApi(),
|
||||
RealDebridProviderApi(),
|
||||
)
|
||||
|
||||
fun apiFor(providerId: String?): DebridProviderApi? {
|
||||
val normalized = DebridProviders.byId(providerId)?.id ?: return null
|
||||
return registered.firstOrNull { it.provider.id == normalized }
|
||||
}
|
||||
}
|
||||
|
||||
private class TorboxDebridProviderApi(
|
||||
private val fileSelector: TorboxFileSelector = TorboxFileSelector(),
|
||||
) : DebridProviderApi {
|
||||
override val provider: DebridProvider = DebridProviders.Torbox
|
||||
|
||||
override suspend fun validateApiKey(apiKey: String): Boolean =
|
||||
TorboxApiClient.validateApiKey(apiKey)
|
||||
|
||||
override suspend fun resolveClientStream(
|
||||
stream: StreamItem,
|
||||
apiKey: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): DirectDebridResolveResult {
|
||||
val resolve = stream.clientResolve ?: return DirectDebridResolveResult.Error
|
||||
val magnet = resolve.magnetUri?.takeIf { it.isNotBlank() }
|
||||
?: buildMagnetUri(resolve)
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
|
||||
return try {
|
||||
val create = TorboxApiClient.createTorrent(apiKey = apiKey, magnet = magnet)
|
||||
val torrentId = create.body?.takeIf { it.success != false }?.data?.resolvedTorrentId()
|
||||
?: return create.toFailureForCreate()
|
||||
|
||||
val torrent = TorboxApiClient.getTorrent(apiKey = apiKey, id = torrentId)
|
||||
if (!torrent.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val files = torrent.body?.data?.files.orEmpty()
|
||||
val file = fileSelector.selectFile(files, resolve, season, episode)
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
val fileId = file.id ?: return DirectDebridResolveResult.Stale
|
||||
|
||||
val link = TorboxApiClient.requestDownloadLink(
|
||||
apiKey = apiKey,
|
||||
torrentId = torrentId,
|
||||
fileId = fileId,
|
||||
)
|
||||
if (!link.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val url = link.body?.data?.takeIf { it.isNotBlank() }
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
|
||||
DirectDebridResolveResult.Success(
|
||||
url = url,
|
||||
filename = file.displayName().takeIf { it.isNotBlank() },
|
||||
videoSize = file.size,
|
||||
)
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
DirectDebridResolveResult.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RealDebridProviderApi(
|
||||
private val fileSelector: RealDebridFileSelector = RealDebridFileSelector(),
|
||||
) : DebridProviderApi {
|
||||
override val provider: DebridProvider = DebridProviders.RealDebrid
|
||||
|
||||
override suspend fun validateApiKey(apiKey: String): Boolean =
|
||||
RealDebridApiClient.validateApiKey(apiKey)
|
||||
|
||||
override suspend fun resolveClientStream(
|
||||
stream: StreamItem,
|
||||
apiKey: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): DirectDebridResolveResult {
|
||||
val resolve = stream.clientResolve ?: return DirectDebridResolveResult.Error
|
||||
val magnet = resolve.magnetUri?.takeIf { it.isNotBlank() }
|
||||
?: buildMagnetUri(resolve)
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
|
||||
return try {
|
||||
val add = RealDebridApiClient.addMagnet(apiKey, magnet)
|
||||
val torrentId = add.body?.id?.takeIf { add.isSuccessful && it.isNotBlank() }
|
||||
?: return add.toFailureForAdd()
|
||||
var resolved = false
|
||||
try {
|
||||
val infoBefore = RealDebridApiClient.getTorrentInfo(apiKey, torrentId)
|
||||
if (!infoBefore.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val filesBefore = infoBefore.body?.files.orEmpty()
|
||||
val file = fileSelector.selectFile(
|
||||
files = filesBefore,
|
||||
resolve = resolve,
|
||||
season = season,
|
||||
episode = episode,
|
||||
) ?: return DirectDebridResolveResult.Stale
|
||||
val fileId = file.id ?: return DirectDebridResolveResult.Stale
|
||||
val select = RealDebridApiClient.selectFiles(apiKey, torrentId, fileId.toString())
|
||||
if (!select.isSuccessful && select.status != 202) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
val infoAfter = RealDebridApiClient.getTorrentInfo(apiKey, torrentId)
|
||||
if (!infoAfter.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val link = infoAfter.body?.firstDownloadLink()
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
val unrestrict = RealDebridApiClient.unrestrictLink(apiKey, link)
|
||||
if (!unrestrict.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val url = unrestrict.body?.download?.takeIf { it.isNotBlank() }
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
resolved = true
|
||||
DirectDebridResolveResult.Success(
|
||||
url = url,
|
||||
filename = unrestrict.body.filename?.takeIf { it.isNotBlank() }
|
||||
?: file.displayName().takeIf { it.isNotBlank() },
|
||||
videoSize = unrestrict.body.filesize ?: file.bytes,
|
||||
)
|
||||
} finally {
|
||||
if (!resolved) {
|
||||
runCatching { RealDebridApiClient.deleteTorrent(apiKey, torrentId) }
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
DirectDebridResolveResult.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMagnetUri(resolve: StreamClientResolve): String? {
|
||||
val hash = resolve.infoHash?.takeIf { it.isNotBlank() } ?: return null
|
||||
return buildString {
|
||||
append("magnet:?xt=urn:btih:")
|
||||
append(hash)
|
||||
resolve.sources
|
||||
.mapNotNull { it.toTrackerUrlOrNull() }
|
||||
.distinct()
|
||||
.forEach { source ->
|
||||
append("&tr=")
|
||||
append(encodePathSegment(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTrackerUrlOrNull(): String? {
|
||||
val value = trim()
|
||||
if (value.isBlank() || value.startsWith("dht:", ignoreCase = true)) return null
|
||||
return value.removePrefix("tracker:").trim().takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun DebridApiResponse<TorboxEnvelopeDto<TorboxCreateTorrentDataDto>>.toFailureForCreate(): DirectDebridResolveResult =
|
||||
when (status) {
|
||||
401, 403 -> DirectDebridResolveResult.Error
|
||||
409 -> DirectDebridResolveResult.NotCached
|
||||
else -> DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
private fun DebridApiResponse<RealDebridAddTorrentDto>.toFailureForAdd(): DirectDebridResolveResult =
|
||||
when (status) {
|
||||
401, 403 -> DirectDebridResolveResult.Error
|
||||
else -> DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
private fun RealDebridTorrentInfoDto.firstDownloadLink(): String? {
|
||||
if (!status.equals("downloaded", ignoreCase = true)) return null
|
||||
return links.orEmpty().firstOrNull { it.isNotBlank() }
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
data class DebridSettings(
|
||||
val enabled: Boolean = false,
|
||||
val torboxApiKey: String = "",
|
||||
val realDebridApiKey: String = "",
|
||||
val providerApiKeys: Map<String, String> = emptyMap(),
|
||||
val instantPlaybackPreparationLimit: Int = 0,
|
||||
val streamMaxResults: Int = 0,
|
||||
val streamSortMode: DebridStreamSortMode = DebridStreamSortMode.DEFAULT,
|
||||
|
|
@ -17,11 +16,24 @@ data class DebridSettings(
|
|||
val streamNameTemplate: String = DebridStreamFormatterDefaults.NAME_TEMPLATE,
|
||||
val streamDescriptionTemplate: String = DebridStreamFormatterDefaults.DESCRIPTION_TEMPLATE,
|
||||
) {
|
||||
val torboxApiKey: String
|
||||
get() = apiKeyFor(DebridProviders.TORBOX_ID)
|
||||
|
||||
val realDebridApiKey: String
|
||||
get() = apiKeyFor(DebridProviders.REAL_DEBRID_ID)
|
||||
|
||||
val hasAnyApiKey: Boolean
|
||||
get() = DebridProviders.configuredServices(this).isNotEmpty()
|
||||
|
||||
val hasCustomStreamFormatting: Boolean
|
||||
get() = streamNameTemplate.isNotBlank() || streamDescriptionTemplate.isNotBlank()
|
||||
|
||||
fun apiKeyFor(providerId: String?): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId?.trim()?.lowercase()
|
||||
?: return ""
|
||||
return providerApiKeys[normalized].orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
const val DEBRID_PREPARE_INSTANT_PLAYBACK_DEFAULT_LIMIT = 2
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ object DebridSettingsRepository {
|
|||
|
||||
private var hasLoaded = false
|
||||
private var enabled = false
|
||||
private var torboxApiKey = ""
|
||||
private var realDebridApiKey = ""
|
||||
private var providerApiKeys = emptyMap<String, String>()
|
||||
private var instantPlaybackPreparationLimit = 0
|
||||
private var streamMaxResults = 0
|
||||
private var streamSortMode = DebridStreamSortMode.DEFAULT
|
||||
|
|
@ -57,24 +56,27 @@ object DebridSettingsRepository {
|
|||
DebridSettingsStorage.saveEnabled(value)
|
||||
}
|
||||
|
||||
fun setTorboxApiKey(value: String) {
|
||||
fun setProviderApiKey(providerId: String, value: String) {
|
||||
ensureLoaded()
|
||||
val provider = DebridProviders.byId(providerId) ?: return
|
||||
val normalized = value.trim()
|
||||
if (torboxApiKey == normalized) return
|
||||
torboxApiKey = normalized
|
||||
if (providerApiKeys[provider.id].orEmpty() == normalized) return
|
||||
providerApiKeys = if (normalized.isBlank()) {
|
||||
providerApiKeys - provider.id
|
||||
} else {
|
||||
providerApiKeys + (provider.id to normalized)
|
||||
}
|
||||
disableIfNoKeys()
|
||||
publish()
|
||||
DebridSettingsStorage.saveTorboxApiKey(normalized)
|
||||
DebridSettingsStorage.saveProviderApiKey(provider.id, normalized)
|
||||
}
|
||||
|
||||
fun setTorboxApiKey(value: String) {
|
||||
setProviderApiKey(DebridProviders.TORBOX_ID, value)
|
||||
}
|
||||
|
||||
fun setRealDebridApiKey(value: String) {
|
||||
ensureLoaded()
|
||||
val normalized = value.trim()
|
||||
if (realDebridApiKey == normalized) return
|
||||
realDebridApiKey = normalized
|
||||
disableIfNoKeys()
|
||||
publish()
|
||||
DebridSettingsStorage.saveRealDebridApiKey(normalized)
|
||||
setProviderApiKey(DebridProviders.REAL_DEBRID_ID, value)
|
||||
}
|
||||
|
||||
fun setInstantPlaybackPreparationLimit(value: Int) {
|
||||
|
|
@ -200,13 +202,20 @@ object DebridSettingsRepository {
|
|||
}
|
||||
|
||||
private fun hasVisibleApiKey(): Boolean =
|
||||
(DebridProviders.isVisible(DebridProviders.TORBOX_ID) && torboxApiKey.isNotBlank()) ||
|
||||
(DebridProviders.isVisible(DebridProviders.REAL_DEBRID_ID) && realDebridApiKey.isNotBlank())
|
||||
DebridProviders.visible().any { provider ->
|
||||
providerApiKeys[provider.id].orEmpty().isNotBlank()
|
||||
}
|
||||
|
||||
private fun loadFromDisk() {
|
||||
hasLoaded = true
|
||||
torboxApiKey = DebridSettingsStorage.loadTorboxApiKey()?.trim().orEmpty()
|
||||
realDebridApiKey = DebridSettingsStorage.loadRealDebridApiKey()?.trim().orEmpty()
|
||||
providerApiKeys = DebridProviders.all()
|
||||
.mapNotNull { provider ->
|
||||
DebridSettingsStorage.loadProviderApiKey(provider.id)
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { apiKey -> provider.id to apiKey }
|
||||
}
|
||||
.toMap()
|
||||
enabled = (DebridSettingsStorage.loadEnabled() ?: false) && hasVisibleApiKey()
|
||||
instantPlaybackPreparationLimit = normalizeDebridInstantPlaybackPreparationLimit(
|
||||
DebridSettingsStorage.loadInstantPlaybackPreparationLimit() ?: 0,
|
||||
|
|
@ -255,8 +264,7 @@ object DebridSettingsRepository {
|
|||
private fun publish() {
|
||||
_uiState.value = DebridSettings(
|
||||
enabled = enabled,
|
||||
torboxApiKey = torboxApiKey,
|
||||
realDebridApiKey = realDebridApiKey,
|
||||
providerApiKeys = providerApiKeys,
|
||||
instantPlaybackPreparationLimit = instantPlaybackPreparationLimit,
|
||||
streamMaxResults = streamMaxResults,
|
||||
streamSortMode = streamSortMode,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import kotlinx.serialization.json.JsonObject
|
|||
internal expect object DebridSettingsStorage {
|
||||
fun loadEnabled(): Boolean?
|
||||
fun saveEnabled(enabled: Boolean)
|
||||
fun loadProviderApiKey(providerId: String): String?
|
||||
fun saveProviderApiKey(providerId: String, apiKey: String)
|
||||
fun loadTorboxApiKey(): String?
|
||||
fun saveTorboxApiKey(apiKey: String)
|
||||
fun loadRealDebridApiKey(): String?
|
||||
|
|
|
|||
|
|
@ -43,15 +43,15 @@ object DebridStreamPresentation {
|
|||
return isAddonDebridCandidate && (isDirectDebridStream || (
|
||||
isTorrentStream &&
|
||||
status != null &&
|
||||
status.providerId == DebridProviders.TORBOX_ID &&
|
||||
DebridProviders.byId(status.providerId)?.supports(DebridProviderCapability.LocalTorrentCacheCheck) == true &&
|
||||
status.state != StreamDebridCacheState.CHECKING
|
||||
))
|
||||
}
|
||||
|
||||
private val StreamItem.isUncachedDebridStream: Boolean
|
||||
get() = isInstalledAddonStream &&
|
||||
debridCacheStatus?.providerId == DebridProviders.TORBOX_ID &&
|
||||
debridCacheStatus.state == StreamDebridCacheState.NOT_CACHED
|
||||
DebridProviders.byId(debridCacheStatus?.providerId)?.supports(DebridProviderCapability.LocalTorrentCacheCheck) == true &&
|
||||
debridCacheStatus?.state == StreamDebridCacheState.NOT_CACHED
|
||||
|
||||
private fun applyLimits(
|
||||
streams: List<Pair<StreamItem, DebridStreamFacts>>,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@ import nuvio.composeapp.generated.resources.debrid_stream_stale
|
|||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
object DirectDebridPlaybackResolver {
|
||||
private val torboxResolver = TorboxDirectDebridResolver()
|
||||
private val torboxAddonStreamResolver = TorboxAddonStreamResolver()
|
||||
private val realDebridResolver = RealDebridDirectDebridResolver()
|
||||
private val localAddonStreamResolver = LocalDebridAddonStreamResolver()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val mutex = Mutex()
|
||||
private val resolvedCache = mutableMapOf<String, CachedDirectDebridResolve>()
|
||||
|
|
@ -115,27 +113,29 @@ object DirectDebridPlaybackResolver {
|
|||
val settings = DebridSettingsRepository.snapshot()
|
||||
if (!settings.enabled) return false
|
||||
if (stream.needsLocalDebridResolve) {
|
||||
return stream.isInstalledAddonStream && settings.torboxApiKey.isNotBlank()
|
||||
return stream.isInstalledAddonStream && localTorrentResolveCredential(settings) != null
|
||||
}
|
||||
if (!stream.isInstalledAddonStream || !stream.isDirectDebridStream || stream.playableDirectUrl != null) {
|
||||
return false
|
||||
}
|
||||
return when (DebridProviders.byId(stream.clientResolve?.service)?.id) {
|
||||
DebridProviders.TORBOX_ID -> settings.torboxApiKey.isNotBlank()
|
||||
DebridProviders.REAL_DEBRID_ID -> settings.realDebridApiKey.isNotBlank()
|
||||
else -> false
|
||||
}
|
||||
val providerId = DebridProviders.byId(stream.clientResolve?.service)?.id ?: return false
|
||||
return settings.apiKeyFor(providerId).isNotBlank() && DebridProviderApis.apiFor(providerId) != null
|
||||
}
|
||||
|
||||
private suspend fun resolveUncached(stream: StreamItem, season: Int?, episode: Int?): DirectDebridResolveResult =
|
||||
when {
|
||||
stream.needsLocalDebridResolve -> torboxAddonStreamResolver.resolve(stream, season, episode)
|
||||
else -> when (DebridProviders.byId(stream.clientResolve?.service)?.id) {
|
||||
DebridProviders.TORBOX_ID -> torboxResolver.resolve(stream, season, episode)
|
||||
DebridProviders.REAL_DEBRID_ID -> realDebridResolver.resolve(stream, season, episode)
|
||||
else -> DirectDebridResolveResult.Error
|
||||
}
|
||||
private suspend fun resolveUncached(stream: StreamItem, season: Int?, episode: Int?): DirectDebridResolveResult {
|
||||
if (stream.needsLocalDebridResolve) {
|
||||
return localAddonStreamResolver.resolve(stream, season, episode)
|
||||
}
|
||||
val providerId = DebridProviders.byId(stream.clientResolve?.service)?.id
|
||||
?: return DirectDebridResolveResult.Error
|
||||
val apiKey = DebridSettingsRepository.snapshot()
|
||||
.apiKeyFor(providerId)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?: return DirectDebridResolveResult.MissingApiKey
|
||||
val api = DebridProviderApis.apiFor(providerId) ?: return DirectDebridResolveResult.Error
|
||||
return api.resolveClientStream(stream, apiKey, season, episode)
|
||||
}
|
||||
|
||||
suspend fun resolveToPlayableStream(
|
||||
stream: StreamItem,
|
||||
|
|
@ -192,21 +192,23 @@ fun DirectDebridPlayableResult.toastMessage(): String? =
|
|||
DirectDebridPlayableResult.Error -> runBlocking { getString(Res.string.debrid_resolve_failed) }
|
||||
}
|
||||
|
||||
private class TorboxAddonStreamResolver(
|
||||
private class LocalDebridAddonStreamResolver(
|
||||
private val fileSelector: TorboxFileSelector = TorboxFileSelector(),
|
||||
) {
|
||||
suspend fun resolve(stream: StreamItem, season: Int?, episode: Int?): DirectDebridResolveResult {
|
||||
val apiKey = DebridSettingsRepository.snapshot().torboxApiKey.trim()
|
||||
if (apiKey.isBlank()) {
|
||||
return DirectDebridResolveResult.MissingApiKey
|
||||
}
|
||||
val account = localTorrentResolveCredential() ?: return DirectDebridResolveResult.MissingApiKey
|
||||
val apiKey = account.apiKey.trim()
|
||||
|
||||
val hash = stream.infoHash?.trim()?.lowercase()
|
||||
if (stream.debridCacheStatus?.state == StreamDebridCacheState.NOT_CACHED) {
|
||||
return DirectDebridResolveResult.NotCached
|
||||
}
|
||||
if (!hash.isNullOrBlank() && stream.debridCacheStatus?.state != StreamDebridCacheState.CACHED) {
|
||||
when (TorboxAvailabilityService.isCached(hash)) {
|
||||
if (
|
||||
!hash.isNullOrBlank() &&
|
||||
stream.debridCacheStatus?.state != StreamDebridCacheState.CACHED &&
|
||||
account.provider.supports(DebridProviderCapability.LocalTorrentCacheCheck)
|
||||
) {
|
||||
when (LocalDebridService.isCached(account, hash)) {
|
||||
false -> return DirectDebridResolveResult.NotCached
|
||||
true, null -> Unit
|
||||
}
|
||||
|
|
@ -214,8 +216,22 @@ private class TorboxAddonStreamResolver(
|
|||
|
||||
val magnet = DebridMagnetBuilder.fromStream(stream)
|
||||
?: return DirectDebridResolveResult.Stale
|
||||
val resolve = stream.toResolveMetadata(season, episode)
|
||||
val resolve = stream.toResolveMetadata(season, episode, account.provider.id)
|
||||
|
||||
return when (account.provider.id) {
|
||||
DebridProviders.TORBOX_ID -> resolveTorbox(stream, resolve, apiKey, magnet, season, episode)
|
||||
else -> DirectDebridResolveResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveTorbox(
|
||||
stream: StreamItem,
|
||||
resolve: StreamClientResolve,
|
||||
apiKey: String,
|
||||
magnet: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): DirectDebridResolveResult {
|
||||
return try {
|
||||
val create = TorboxApiClient.createTorrent(apiKey = apiKey, magnet = magnet)
|
||||
val torrentId = create.body?.takeIf { it.success != false }?.data?.resolvedTorrentId()
|
||||
|
|
@ -254,184 +270,20 @@ private class TorboxAddonStreamResolver(
|
|||
}
|
||||
}
|
||||
|
||||
private class TorboxDirectDebridResolver(
|
||||
private val fileSelector: TorboxFileSelector = TorboxFileSelector(),
|
||||
) {
|
||||
suspend fun resolve(stream: StreamItem, season: Int?, episode: Int?): DirectDebridResolveResult {
|
||||
val resolve = stream.clientResolve ?: return DirectDebridResolveResult.Error
|
||||
val apiKey = DebridSettingsRepository.snapshot().torboxApiKey.trim()
|
||||
if (apiKey.isBlank()) {
|
||||
return DirectDebridResolveResult.MissingApiKey
|
||||
}
|
||||
val magnet = resolve.magnetUri?.takeIf { it.isNotBlank() }
|
||||
?: buildMagnetUri(resolve)
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
return try {
|
||||
val create = TorboxApiClient.createTorrent(apiKey = apiKey, magnet = magnet)
|
||||
val torrentId = create.body?.takeIf { it.success != false }?.data?.resolvedTorrentId()
|
||||
?: return create.toFailureForCreate()
|
||||
|
||||
val torrent = TorboxApiClient.getTorrent(apiKey = apiKey, id = torrentId)
|
||||
if (!torrent.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val files = torrent.body?.data?.files.orEmpty()
|
||||
val file = fileSelector.selectFile(files, resolve, season, episode)
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val fileId = file.id
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
val link = TorboxApiClient.requestDownloadLink(
|
||||
apiKey = apiKey,
|
||||
torrentId = torrentId,
|
||||
fileId = fileId,
|
||||
)
|
||||
if (!link.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val url = link.body?.data?.takeIf { it.isNotBlank() }
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
DirectDebridResolveResult.Success(
|
||||
url = url,
|
||||
filename = file.displayName().takeIf { it.isNotBlank() },
|
||||
videoSize = file.size,
|
||||
)
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
DirectDebridResolveResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class RealDebridDirectDebridResolver(
|
||||
private val fileSelector: RealDebridFileSelector = RealDebridFileSelector(),
|
||||
) {
|
||||
suspend fun resolve(stream: StreamItem, season: Int?, episode: Int?): DirectDebridResolveResult {
|
||||
val resolve = stream.clientResolve ?: return DirectDebridResolveResult.Error
|
||||
val apiKey = DebridSettingsRepository.snapshot().realDebridApiKey.trim()
|
||||
if (apiKey.isBlank()) {
|
||||
return DirectDebridResolveResult.MissingApiKey
|
||||
}
|
||||
val magnet = resolve.magnetUri?.takeIf { it.isNotBlank() }
|
||||
?: buildMagnetUri(resolve)
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
return try {
|
||||
val add = RealDebridApiClient.addMagnet(apiKey, magnet)
|
||||
val torrentId = add.body?.id?.takeIf { add.isSuccessful && it.isNotBlank() }
|
||||
?: return add.toFailureForAdd()
|
||||
var resolved = false
|
||||
try {
|
||||
val infoBefore = RealDebridApiClient.getTorrentInfo(apiKey, torrentId)
|
||||
if (!infoBefore.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val filesBefore = infoBefore.body?.files.orEmpty()
|
||||
val file = fileSelector.selectFile(
|
||||
files = filesBefore,
|
||||
resolve = resolve,
|
||||
season = season,
|
||||
episode = episode,
|
||||
)
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val fileId = file.id
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val select = RealDebridApiClient.selectFiles(apiKey, torrentId, fileId.toString())
|
||||
if (!select.isSuccessful && select.status != 202) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
val infoAfter = RealDebridApiClient.getTorrentInfo(apiKey, torrentId)
|
||||
if (!infoAfter.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val link = infoAfter.body?.firstDownloadLink()
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val unrestrict = RealDebridApiClient.unrestrictLink(apiKey, link)
|
||||
if (!unrestrict.isSuccessful) {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
val url = unrestrict.body?.download?.takeIf { it.isNotBlank() }
|
||||
?: run {
|
||||
return DirectDebridResolveResult.Stale
|
||||
}
|
||||
resolved = true
|
||||
DirectDebridResolveResult.Success(
|
||||
url = url,
|
||||
filename = unrestrict.body.filename?.takeIf { it.isNotBlank() }
|
||||
?: file.displayName().takeIf { it.isNotBlank() },
|
||||
videoSize = unrestrict.body.filesize ?: file.bytes,
|
||||
)
|
||||
} finally {
|
||||
if (!resolved) {
|
||||
runCatching { RealDebridApiClient.deleteTorrent(apiKey, torrentId) }
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
DirectDebridResolveResult.Error
|
||||
}
|
||||
}
|
||||
|
||||
private fun DebridApiResponse<RealDebridAddTorrentDto>.toFailureForAdd(): DirectDebridResolveResult =
|
||||
when (status) {
|
||||
401, 403 -> DirectDebridResolveResult.Error
|
||||
else -> DirectDebridResolveResult.Stale
|
||||
}
|
||||
|
||||
private fun RealDebridTorrentInfoDto.firstDownloadLink(): String? {
|
||||
if (!status.equals("downloaded", ignoreCase = true)) return null
|
||||
return links.orEmpty().firstOrNull { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMagnetUri(resolve: StreamClientResolve): String? {
|
||||
val hash = resolve.infoHash?.takeIf { it.isNotBlank() } ?: return null
|
||||
return buildString {
|
||||
append("magnet:?xt=urn:btih:")
|
||||
append(hash)
|
||||
resolve.sources
|
||||
.mapNotNull { it.toTrackerUrlOrNull() }
|
||||
.distinct()
|
||||
.forEach { source ->
|
||||
append("&tr=")
|
||||
append(encodePathSegment(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTrackerUrlOrNull(): String? {
|
||||
val value = trim()
|
||||
if (value.isBlank() || value.startsWith("dht:", ignoreCase = true)) return null
|
||||
return value.removePrefix("tracker:").trim().takeIf { it.isNotBlank() }
|
||||
}
|
||||
private fun localTorrentResolveCredential(
|
||||
settings: DebridSettings = DebridSettingsRepository.snapshot(),
|
||||
): DebridServiceCredential? =
|
||||
DebridProviders.configuredServices(settings)
|
||||
.firstOrNull { credential -> credential.provider.supports(DebridProviderCapability.LocalTorrentResolve) }
|
||||
|
||||
private fun StreamItem.debridResolveCacheKey(season: Int?, episode: Int?): String? {
|
||||
val resolve = clientResolve
|
||||
if (resolve == null && needsLocalDebridResolve) {
|
||||
val apiKey = DebridSettingsRepository.snapshot().torboxApiKey.trim().takeIf { it.isNotBlank() } ?: return null
|
||||
val account = localTorrentResolveCredential() ?: return null
|
||||
val apiKey = account.apiKey.trim().takeIf { it.isNotBlank() } ?: return null
|
||||
val identity = infoHash ?: torrentMagnetUri ?: behaviorHints.filename ?: return null
|
||||
return listOf(
|
||||
DebridProviders.TORBOX_ID,
|
||||
account.provider.id,
|
||||
apiKey.stableFingerprint(),
|
||||
identity.trim().lowercase(),
|
||||
fileIdx?.toString().orEmpty(),
|
||||
|
|
@ -442,11 +294,11 @@ private fun StreamItem.debridResolveCacheKey(season: Int?, episode: Int?): Strin
|
|||
}
|
||||
resolve ?: return null
|
||||
val providerId = DebridProviders.byId(resolve.service)?.id ?: return null
|
||||
val apiKey = when (providerId) {
|
||||
DebridProviders.TORBOX_ID -> DebridSettingsRepository.snapshot().torboxApiKey
|
||||
DebridProviders.REAL_DEBRID_ID -> DebridSettingsRepository.snapshot().realDebridApiKey
|
||||
else -> ""
|
||||
}.trim().takeIf { it.isNotBlank() } ?: return null
|
||||
val apiKey = DebridSettingsRepository.snapshot()
|
||||
.apiKeyFor(providerId)
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?: return null
|
||||
val identity = resolve.infoHash
|
||||
?: resolve.magnetUri
|
||||
?: resolve.torrentName
|
||||
|
|
@ -464,7 +316,7 @@ private fun StreamItem.debridResolveCacheKey(season: Int?, episode: Int?): Strin
|
|||
).joinToString("|")
|
||||
}
|
||||
|
||||
private fun StreamItem.toResolveMetadata(season: Int?, episode: Int?): StreamClientResolve =
|
||||
private fun StreamItem.toResolveMetadata(season: Int?, episode: Int?, providerId: String): StreamClientResolve =
|
||||
StreamClientResolve(
|
||||
type = "torrent",
|
||||
infoHash = infoHash,
|
||||
|
|
@ -475,7 +327,7 @@ private fun StreamItem.toResolveMetadata(season: Int?, episode: Int?): StreamCli
|
|||
filename = behaviorHints.filename,
|
||||
season = season,
|
||||
episode = episode,
|
||||
service = DebridProviders.TORBOX_ID,
|
||||
service = providerId,
|
||||
isCached = debridCacheStatus?.state == StreamDebridCacheState.CACHED,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ object DirectDebridStreamPreparer {
|
|||
}
|
||||
|
||||
if (!consumeBackgroundBudget()) {
|
||||
log.d { "Skipping instant playback preparation; local Torbox budget reached" }
|
||||
log.d { "Skipping instant playback preparation; local debrid budget reached" }
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,23 +4,21 @@ import com.nuvio.app.features.streams.AddonStreamGroup
|
|||
import com.nuvio.app.features.streams.StreamDebridCacheState
|
||||
import com.nuvio.app.features.streams.StreamDebridCacheStatus
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
object TorboxAvailabilityService {
|
||||
object LocalDebridAvailabilityService {
|
||||
fun markChecking(
|
||||
groups: List<AddonStreamGroup>,
|
||||
eligibleGroupIds: Set<String>? = null,
|
||||
): List<AddonStreamGroup> {
|
||||
val settings = DebridSettingsRepository.snapshot()
|
||||
if (!settings.enabled || settings.torboxApiKey.isBlank()) return groups
|
||||
val account = cacheCheckAccount() ?: return groups
|
||||
return groups.updateAvailabilityStatus(eligibleGroupIds) { stream ->
|
||||
if (stream.torboxAvailabilityHash() == null || stream.debridCacheStatus?.state == StreamDebridCacheState.CACHED) {
|
||||
if (stream.localAvailabilityHash() == null || stream.debridCacheStatus?.state == StreamDebridCacheState.CACHED) {
|
||||
stream
|
||||
} else {
|
||||
stream.copy(
|
||||
debridCacheStatus = StreamDebridCacheStatus(
|
||||
providerId = DebridProviders.TORBOX_ID,
|
||||
providerName = DebridProviders.Torbox.displayName,
|
||||
providerId = account.provider.id,
|
||||
providerName = account.provider.displayName,
|
||||
state = StreamDebridCacheState.CHECKING,
|
||||
),
|
||||
)
|
||||
|
|
@ -32,31 +30,28 @@ object TorboxAvailabilityService {
|
|||
groups: List<AddonStreamGroup>,
|
||||
eligibleGroupIds: Set<String>? = null,
|
||||
): List<AddonStreamGroup> {
|
||||
val settings = DebridSettingsRepository.snapshot()
|
||||
val apiKey = settings.torboxApiKey.trim()
|
||||
if (!settings.enabled || apiKey.isBlank()) return groups
|
||||
|
||||
val account = cacheCheckAccount() ?: return groups
|
||||
val hashes = groups
|
||||
.filter { group -> eligibleGroupIds == null || group.addonId in eligibleGroupIds }
|
||||
.flatMap { group ->
|
||||
group.streams.mapNotNull { stream ->
|
||||
stream.torboxAvailabilityHash()
|
||||
stream.localAvailabilityHash()
|
||||
?.takeUnless { stream.debridCacheStatus?.state in FINAL_CACHE_STATES }
|
||||
}
|
||||
}
|
||||
.distinct()
|
||||
if (hashes.isEmpty()) return groups
|
||||
|
||||
val cached = checkCached(apiKey = apiKey, hashes = hashes)
|
||||
val cached = LocalDebridService.checkCached(account = account, hashes = hashes)
|
||||
?: return groups.updateAvailabilityStatus(eligibleGroupIds) { stream ->
|
||||
val hash = stream.torboxAvailabilityHash()
|
||||
val hash = stream.localAvailabilityHash()
|
||||
if (hash == null) {
|
||||
stream
|
||||
} else {
|
||||
stream.copy(
|
||||
debridCacheStatus = StreamDebridCacheStatus(
|
||||
providerId = DebridProviders.TORBOX_ID,
|
||||
providerName = DebridProviders.Torbox.displayName,
|
||||
providerId = account.provider.id,
|
||||
providerName = account.provider.displayName,
|
||||
state = StreamDebridCacheState.UNKNOWN,
|
||||
),
|
||||
)
|
||||
|
|
@ -64,13 +59,13 @@ object TorboxAvailabilityService {
|
|||
}
|
||||
|
||||
return groups.updateAvailabilityStatus(eligibleGroupIds) { stream ->
|
||||
val hash = stream.torboxAvailabilityHash() ?: return@updateAvailabilityStatus stream
|
||||
val hash = stream.localAvailabilityHash() ?: return@updateAvailabilityStatus stream
|
||||
if (stream.debridCacheStatus?.state in FINAL_CACHE_STATES) return@updateAvailabilityStatus stream
|
||||
val cachedItem = cached[hash]
|
||||
stream.copy(
|
||||
debridCacheStatus = StreamDebridCacheStatus(
|
||||
providerId = DebridProviders.TORBOX_ID,
|
||||
providerName = DebridProviders.Torbox.displayName,
|
||||
providerId = account.provider.id,
|
||||
providerName = account.provider.displayName,
|
||||
state = if (cachedItem == null) StreamDebridCacheState.NOT_CACHED else StreamDebridCacheState.CACHED,
|
||||
cachedName = cachedItem?.name,
|
||||
cachedSize = cachedItem?.size,
|
||||
|
|
@ -80,28 +75,16 @@ object TorboxAvailabilityService {
|
|||
}
|
||||
|
||||
suspend fun isCached(hash: String): Boolean? {
|
||||
val settings = DebridSettingsRepository.snapshot()
|
||||
val apiKey = settings.torboxApiKey.trim()
|
||||
val normalizedHash = hash.trim().lowercase().takeIf { it.isNotBlank() } ?: return null
|
||||
if (!settings.enabled || apiKey.isBlank()) return null
|
||||
return checkCached(apiKey = apiKey, hashes = listOf(normalizedHash))?.containsKey(normalizedHash)
|
||||
val account = cacheCheckAccount() ?: return null
|
||||
return LocalDebridService.isCached(account, hash)
|
||||
}
|
||||
|
||||
private suspend fun checkCached(
|
||||
apiKey: String,
|
||||
hashes: List<String>,
|
||||
): Map<String, TorboxCachedItemDto>? =
|
||||
try {
|
||||
val response = TorboxApiClient.checkCached(apiKey = apiKey, hashes = hashes)
|
||||
if (!response.isSuccessful || response.body?.success == false) {
|
||||
null
|
||||
} else {
|
||||
response.body?.data.orEmpty().mapKeys { it.key.lowercase() }
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
null
|
||||
}
|
||||
private fun cacheCheckAccount(): DebridServiceCredential? {
|
||||
val settings = DebridSettingsRepository.snapshot()
|
||||
if (!settings.enabled) return null
|
||||
return DebridProviders.configuredServices(settings)
|
||||
.firstOrNull { credential -> credential.provider.supports(DebridProviderCapability.LocalTorrentCacheCheck) }
|
||||
}
|
||||
}
|
||||
|
||||
private val FINAL_CACHE_STATES = setOf(
|
||||
|
|
@ -109,7 +92,7 @@ private val FINAL_CACHE_STATES = setOf(
|
|||
StreamDebridCacheState.NOT_CACHED,
|
||||
)
|
||||
|
||||
internal fun StreamItem.torboxAvailabilityHash(): String? =
|
||||
internal fun StreamItem.localAvailabilityHash(): String? =
|
||||
infoHash
|
||||
?.trim()
|
||||
?.lowercase()
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
internal data class LocalDebridCachedItem(
|
||||
val name: String?,
|
||||
val size: Long?,
|
||||
)
|
||||
|
||||
internal object LocalDebridService {
|
||||
suspend fun checkCached(
|
||||
account: DebridServiceCredential,
|
||||
hashes: List<String>,
|
||||
): Map<String, LocalDebridCachedItem>? =
|
||||
when (account.provider.id) {
|
||||
DebridProviders.TORBOX_ID -> checkTorboxCached(account.apiKey, hashes)
|
||||
else -> null
|
||||
}
|
||||
|
||||
suspend fun isCached(account: DebridServiceCredential, hash: String): Boolean? {
|
||||
val normalizedHash = hash.trim().lowercase().takeIf { it.isNotBlank() } ?: return null
|
||||
return checkCached(account, listOf(normalizedHash))?.containsKey(normalizedHash)
|
||||
}
|
||||
|
||||
private suspend fun checkTorboxCached(
|
||||
apiKey: String,
|
||||
hashes: List<String>,
|
||||
): Map<String, LocalDebridCachedItem>? =
|
||||
try {
|
||||
val response = TorboxApiClient.checkCached(apiKey = apiKey, hashes = hashes)
|
||||
if (!response.isSuccessful || response.body?.success == false) {
|
||||
null
|
||||
} else {
|
||||
response.body?.data.orEmpty().mapKeys { it.key.lowercase() }.mapValues { (_, value) ->
|
||||
LocalDebridCachedItem(
|
||||
name = value.name,
|
||||
size = value.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (error is CancellationException) throw error
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.nuvio.app.features.addons.httpGetText
|
|||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridStreamPresentation
|
||||
import com.nuvio.app.features.debrid.DirectDebridStreamPreparer
|
||||
import com.nuvio.app.features.debrid.TorboxAvailabilityService
|
||||
import com.nuvio.app.features.debrid.LocalDebridAvailabilityService
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.plugins.PluginRepository
|
||||
import com.nuvio.app.features.plugins.pluginContentId
|
||||
|
|
@ -274,14 +274,14 @@ object PlayerStreamsRepository {
|
|||
if (group.addonId !in installedAddonIds || group.streams.isEmpty()) return
|
||||
|
||||
val eligibleGroupIds = setOf(group.addonId)
|
||||
val checkingGroup = TorboxAvailabilityService.markChecking(
|
||||
val checkingGroup = LocalDebridAvailabilityService.markChecking(
|
||||
groups = listOf(group),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: group
|
||||
publishStreamGroup(checkingGroup)
|
||||
|
||||
val availabilityJob = launch {
|
||||
val availabilityGroup = TorboxAvailabilityService.annotateCachedAvailability(
|
||||
val availabilityGroup = LocalDebridAvailabilityService.annotateCachedAvailability(
|
||||
groups = listOf(checkingGroup),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: checkingGroup
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import nuvio.composeapp.generated.resources.settings_debrid_key_invalid
|
|||
import nuvio.composeapp.generated.resources.settings_debrid_name_template
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_name_template_description
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_not_set
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_provider_torbox_description
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_provider_description
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_section_instant_playback
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_section_formatting
|
||||
import nuvio.composeapp.generated.resources.settings_debrid_section_providers
|
||||
|
|
@ -127,35 +127,43 @@ internal fun LazyListScope.debridSettingsContent(
|
|||
}
|
||||
|
||||
item {
|
||||
var showApiKeyDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var activeProviderId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val providers = remember { DebridProviders.visible() }
|
||||
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_debrid_section_providers),
|
||||
isTablet = isTablet,
|
||||
) {
|
||||
SettingsGroup(isTablet = isTablet) {
|
||||
DebridPreferenceRow(
|
||||
isTablet = isTablet,
|
||||
title = DebridProviders.Torbox.displayName,
|
||||
description = stringResource(Res.string.settings_debrid_provider_torbox_description),
|
||||
value = maskDebridApiKey(settings.torboxApiKey, stringResource(Res.string.settings_debrid_not_set)),
|
||||
enabled = true,
|
||||
onClick = { showApiKeyDialog = true },
|
||||
)
|
||||
providers.forEachIndexed { index, provider ->
|
||||
if (index > 0) {
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
}
|
||||
DebridPreferenceRow(
|
||||
isTablet = isTablet,
|
||||
title = provider.displayName,
|
||||
description = stringResource(Res.string.settings_debrid_provider_description, provider.displayName),
|
||||
value = maskDebridApiKey(settings.apiKeyFor(provider.id), stringResource(Res.string.settings_debrid_not_set)),
|
||||
enabled = true,
|
||||
onClick = { activeProviderId = provider.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showApiKeyDialog) {
|
||||
DebridApiKeyDialog(
|
||||
providerId = DebridProviders.TORBOX_ID,
|
||||
title = stringResource(Res.string.settings_debrid_dialog_title),
|
||||
subtitle = stringResource(Res.string.settings_debrid_dialog_subtitle),
|
||||
placeholder = stringResource(Res.string.settings_debrid_dialog_placeholder),
|
||||
currentValue = settings.torboxApiKey,
|
||||
onSave = DebridSettingsRepository::setTorboxApiKey,
|
||||
onDismiss = { showApiKeyDialog = false },
|
||||
)
|
||||
}
|
||||
activeProviderId
|
||||
?.let(DebridProviders::byId)
|
||||
?.let { provider ->
|
||||
DebridApiKeyDialog(
|
||||
providerId = provider.id,
|
||||
title = stringResource(Res.string.settings_debrid_dialog_title, provider.displayName),
|
||||
subtitle = stringResource(Res.string.settings_debrid_dialog_subtitle, provider.displayName),
|
||||
placeholder = stringResource(Res.string.settings_debrid_dialog_placeholder, provider.displayName),
|
||||
currentValue = settings.apiKeyFor(provider.id),
|
||||
onSave = { apiKey -> DebridSettingsRepository.setProviderApiKey(provider.id, apiKey) },
|
||||
onDismiss = { activeProviderId = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.nuvio.app.features.debrid.DebridSettings
|
|||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridStreamPresentation
|
||||
import com.nuvio.app.features.debrid.DirectDebridStreamPreparer
|
||||
import com.nuvio.app.features.debrid.TorboxAvailabilityService
|
||||
import com.nuvio.app.features.debrid.LocalDebridAvailabilityService
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -120,11 +120,11 @@ object AddonStreamWarmupRepository {
|
|||
videoId = key.videoId,
|
||||
)
|
||||
val eligibleGroupIds = setOf(group.addonId)
|
||||
val checkingGroup = TorboxAvailabilityService.markChecking(
|
||||
val checkingGroup = LocalDebridAvailabilityService.markChecking(
|
||||
groups = listOf(group),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: group
|
||||
val availabilityGroup = TorboxAvailabilityService.annotateCachedAvailability(
|
||||
val availabilityGroup = LocalDebridAvailabilityService.annotateCachedAvailability(
|
||||
groups = listOf(checkingGroup),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: checkingGroup
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.nuvio.app.features.addons.httpGetText
|
|||
import com.nuvio.app.features.debrid.DirectDebridStreamPreparer
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridStreamPresentation
|
||||
import com.nuvio.app.features.debrid.TorboxAvailabilityService
|
||||
import com.nuvio.app.features.debrid.LocalDebridAvailabilityService
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.plugins.PluginRepository
|
||||
|
|
@ -265,14 +265,14 @@ object StreamsRepository {
|
|||
if (group.addonId !in installedAddonIds || group.streams.isEmpty()) return
|
||||
|
||||
val eligibleGroupIds = setOf(group.addonId)
|
||||
val checkingGroup = TorboxAvailabilityService.markChecking(
|
||||
val checkingGroup = LocalDebridAvailabilityService.markChecking(
|
||||
groups = listOf(group),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: group
|
||||
publishAddonGroup(checkingGroup)
|
||||
|
||||
val availabilityJob = launch {
|
||||
val availabilityGroup = TorboxAvailabilityService.annotateCachedAvailability(
|
||||
val availabilityGroup = LocalDebridAvailabilityService.annotateCachedAvailability(
|
||||
groups = listOf(checkingGroup),
|
||||
eligibleGroupIds = eligibleGroupIds,
|
||||
).firstOrNull() ?: checkingGroup
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DebridProviderTest {
|
||||
@Test
|
||||
fun `torbox exposes local addon capabilities`() {
|
||||
assertTrue(DebridProviders.Torbox.supports(DebridProviderCapability.ClientResolve))
|
||||
assertTrue(DebridProviders.Torbox.supports(DebridProviderCapability.LocalTorrentCacheCheck))
|
||||
assertTrue(DebridProviders.Torbox.supports(DebridProviderCapability.LocalTorrentResolve))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `real debrid stays hidden from local addon capability paths`() {
|
||||
assertFalse(DebridProviders.RealDebrid.visibleInUi)
|
||||
assertTrue(DebridProviders.RealDebrid.supports(DebridProviderCapability.ClientResolve))
|
||||
assertFalse(DebridProviders.RealDebrid.supports(DebridProviderCapability.LocalTorrentCacheCheck))
|
||||
assertFalse(DebridProviders.RealDebrid.supports(DebridProviderCapability.LocalTorrentResolve))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.features.debrid
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DebridSettingsTest {
|
||||
@Test
|
||||
fun `normalizes provider ids when reading api keys`() {
|
||||
val settings = DebridSettings(
|
||||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "tb_key"),
|
||||
)
|
||||
|
||||
assertEquals("tb_key", settings.apiKeyFor("TORBOX"))
|
||||
assertEquals("tb_key", settings.torboxApiKey)
|
||||
assertEquals("", settings.realDebridApiKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `configured services are driven by visible registered providers`() {
|
||||
val settings = DebridSettings(
|
||||
providerApiKeys = mapOf(
|
||||
DebridProviders.TORBOX_ID to "tb_key",
|
||||
DebridProviders.REAL_DEBRID_ID to "rd_key",
|
||||
),
|
||||
)
|
||||
|
||||
val services = DebridProviders.configuredServices(settings)
|
||||
|
||||
assertEquals(listOf(DebridProviders.TORBOX_ID), services.map { it.provider.id })
|
||||
assertEquals("tb_key", services.single().apiKey)
|
||||
assertTrue(settings.hasAnyApiKey)
|
||||
assertFalse(DebridProviders.isVisible(DebridProviders.REAL_DEBRID_ID))
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class DebridStreamPresentationTest {
|
|||
stream = stream,
|
||||
settings = DebridSettings(
|
||||
enabled = true,
|
||||
torboxApiKey = "key",
|
||||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "key"),
|
||||
streamNameTemplate = "{stream.resolution} {service.shortName} {service.cached::istrue[\"Ready\"||\"Not Ready\"]}",
|
||||
streamDescriptionTemplate = "{stream.quality} {stream.encode}\n{stream.size::bytes}\n{stream.filename}",
|
||||
),
|
||||
|
|
@ -67,7 +67,7 @@ class DebridStreamPresentationTest {
|
|||
groups = listOf(group),
|
||||
settings = DebridSettings(
|
||||
enabled = true,
|
||||
torboxApiKey = "key",
|
||||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "key"),
|
||||
streamMaxResults = 2,
|
||||
streamSortMode = DebridStreamSortMode.QUALITY_DESC,
|
||||
streamMinimumQuality = DebridStreamMinimumQuality.P1080,
|
||||
|
|
@ -102,7 +102,7 @@ class DebridStreamPresentationTest {
|
|||
),
|
||||
settings = DebridSettings(
|
||||
enabled = true,
|
||||
torboxApiKey = "key",
|
||||
providerApiKeys = mapOf(DebridProviders.TORBOX_ID to "key"),
|
||||
),
|
||||
).single().streams
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ class DirectDebridStreamPreparerTest {
|
|||
StreamItem(
|
||||
name = name,
|
||||
url = url,
|
||||
addonName = "Torbox Instant",
|
||||
addonId = "debrid:torbox",
|
||||
addonName = "Addon",
|
||||
addonId = "addon:test",
|
||||
clientResolve = StreamClientResolve(
|
||||
type = "debrid",
|
||||
service = DebridProviders.TORBOX_ID,
|
||||
|
|
|
|||
|
|
@ -26,21 +26,20 @@ actual object DebridSettingsStorage {
|
|||
private const val streamPreferencesKey = "debrid_stream_preferences"
|
||||
private const val streamNameTemplateKey = "debrid_stream_name_template"
|
||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||
private val syncKeys = listOf(
|
||||
enabledKey,
|
||||
torboxApiKeyKey,
|
||||
realDebridApiKeyKey,
|
||||
instantPlaybackPreparationLimitKey,
|
||||
streamMaxResultsKey,
|
||||
streamSortModeKey,
|
||||
streamMinimumQualityKey,
|
||||
streamDolbyVisionFilterKey,
|
||||
streamHdrFilterKey,
|
||||
streamCodecFilterKey,
|
||||
streamPreferencesKey,
|
||||
streamNameTemplateKey,
|
||||
streamDescriptionTemplateKey,
|
||||
)
|
||||
private fun syncKeys(): List<String> =
|
||||
listOf(
|
||||
enabledKey,
|
||||
instantPlaybackPreparationLimitKey,
|
||||
streamMaxResultsKey,
|
||||
streamSortModeKey,
|
||||
streamMinimumQualityKey,
|
||||
streamDolbyVisionFilterKey,
|
||||
streamHdrFilterKey,
|
||||
streamCodecFilterKey,
|
||||
streamPreferencesKey,
|
||||
streamNameTemplateKey,
|
||||
streamDescriptionTemplateKey,
|
||||
) + DebridProviders.all().map { providerApiKeyKey(it.id) }
|
||||
|
||||
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
|
||||
|
||||
|
|
@ -48,16 +47,23 @@ actual object DebridSettingsStorage {
|
|||
saveBoolean(enabledKey, enabled)
|
||||
}
|
||||
|
||||
actual fun loadTorboxApiKey(): String? = loadString(torboxApiKeyKey)
|
||||
actual fun loadProviderApiKey(providerId: String): String? =
|
||||
loadString(providerApiKeyKey(providerId))
|
||||
|
||||
actual fun saveTorboxApiKey(apiKey: String) {
|
||||
saveString(torboxApiKeyKey, apiKey)
|
||||
actual fun saveProviderApiKey(providerId: String, apiKey: String) {
|
||||
saveString(providerApiKeyKey(providerId), apiKey)
|
||||
}
|
||||
|
||||
actual fun loadRealDebridApiKey(): String? = loadString(realDebridApiKeyKey)
|
||||
actual fun loadTorboxApiKey(): String? = loadProviderApiKey(DebridProviders.TORBOX_ID)
|
||||
|
||||
actual fun saveTorboxApiKey(apiKey: String) {
|
||||
saveProviderApiKey(DebridProviders.TORBOX_ID, apiKey)
|
||||
}
|
||||
|
||||
actual fun loadRealDebridApiKey(): String? = loadProviderApiKey(DebridProviders.REAL_DEBRID_ID)
|
||||
|
||||
actual fun saveRealDebridApiKey(apiKey: String) {
|
||||
saveString(realDebridApiKeyKey, apiKey)
|
||||
saveProviderApiKey(DebridProviders.REAL_DEBRID_ID, apiKey)
|
||||
}
|
||||
|
||||
actual fun loadInstantPlaybackPreparationLimit(): Int? = loadInt(instantPlaybackPreparationLimitKey)
|
||||
|
|
@ -157,8 +163,11 @@ actual object DebridSettingsStorage {
|
|||
|
||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
|
||||
loadTorboxApiKey()?.let { put(torboxApiKeyKey, encodeSyncString(it)) }
|
||||
loadRealDebridApiKey()?.let { put(realDebridApiKeyKey, encodeSyncString(it)) }
|
||||
DebridProviders.all().forEach { provider ->
|
||||
loadProviderApiKey(provider.id)?.let {
|
||||
put(providerApiKeyKey(provider.id), encodeSyncString(it))
|
||||
}
|
||||
}
|
||||
loadInstantPlaybackPreparationLimit()?.let { put(instantPlaybackPreparationLimitKey, encodeSyncInt(it)) }
|
||||
loadStreamMaxResults()?.let { put(streamMaxResultsKey, encodeSyncInt(it)) }
|
||||
loadStreamSortMode()?.let { put(streamSortModeKey, encodeSyncString(it)) }
|
||||
|
|
@ -172,13 +181,16 @@ actual object DebridSettingsStorage {
|
|||
}
|
||||
|
||||
actual fun replaceFromSyncPayload(payload: JsonObject) {
|
||||
syncKeys.forEach { key ->
|
||||
syncKeys().forEach { key ->
|
||||
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
|
||||
}
|
||||
|
||||
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
|
||||
payload.decodeSyncString(torboxApiKeyKey)?.let(::saveTorboxApiKey)
|
||||
payload.decodeSyncString(realDebridApiKeyKey)?.let(::saveRealDebridApiKey)
|
||||
DebridProviders.all().forEach { provider ->
|
||||
payload.decodeSyncString(providerApiKeyKey(provider.id))?.let { apiKey ->
|
||||
saveProviderApiKey(provider.id, apiKey)
|
||||
}
|
||||
}
|
||||
payload.decodeSyncInt(instantPlaybackPreparationLimitKey)?.let(::saveInstantPlaybackPreparationLimit)
|
||||
payload.decodeSyncInt(streamMaxResultsKey)?.let(::saveStreamMaxResults)
|
||||
payload.decodeSyncString(streamSortModeKey)?.let(::saveStreamSortMode)
|
||||
|
|
@ -190,4 +202,14 @@ actual object DebridSettingsStorage {
|
|||
payload.decodeSyncString(streamNameTemplateKey)?.let(::saveStreamNameTemplate)
|
||||
payload.decodeSyncString(streamDescriptionTemplateKey)?.let(::saveStreamDescriptionTemplate)
|
||||
}
|
||||
|
||||
private fun providerApiKeyKey(providerId: String): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId.trim().lowercase().replace(Regex("[^a-z0-9_]+"), "_")
|
||||
return when (normalized) {
|
||||
DebridProviders.TORBOX_ID -> torboxApiKeyKey
|
||||
DebridProviders.REAL_DEBRID_ID -> realDebridApiKeyKey
|
||||
else -> "debrid_${normalized}_api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue