mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 16:49:28 +00:00
feat: plugin support for desktop
This commit is contained in:
parent
3c6bb75fd0
commit
3629141365
10 changed files with 150 additions and 47 deletions
|
|
@ -241,6 +241,7 @@ val iosDistributionSourceDir = if (iosDistribution == "full") {
|
|||
}
|
||||
val iosFrameworkBundleId = "com.nuvio.media"
|
||||
val fullCommonSourceDir = project.file("src/fullCommonMain/kotlin")
|
||||
val fullPluginSourceDir = fullCommonSourceDir.resolve("com/nuvio/app/features/plugins")
|
||||
val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-config/kotlin")
|
||||
val requestedGradleTasks = gradle.startParameter.taskNames.map { taskName ->
|
||||
taskName.substringAfterLast(':').lowercase()
|
||||
|
|
@ -692,10 +693,13 @@ kotlin {
|
|||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar"))))
|
||||
}
|
||||
val desktopMain by getting {
|
||||
kotlin.srcDir(fullPluginSourceDir)
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.quickjs.kt)
|
||||
implementation(libs.ksoup)
|
||||
}
|
||||
}
|
||||
commonMain.dependencies {
|
||||
|
|
|
|||
|
|
@ -26,4 +26,6 @@ internal object PluginStorage {
|
|||
|
||||
internal fun currentPluginPlatform(): String = "android"
|
||||
|
||||
internal fun currentPluginPlatformTags(): Set<String> = setOf(currentPluginPlatform())
|
||||
|
||||
internal fun currentEpochMillis(): Long = System.currentTimeMillis()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.nuvio.app.core.build
|
||||
|
||||
actual object AppFeaturePolicy {
|
||||
actual val pluginsEnabled: Boolean = false
|
||||
actual val pluginsEnabled: Boolean = true
|
||||
actual val downloadsEnabled: Boolean = false
|
||||
actual val notificationsEnabled: Boolean = false
|
||||
actual val p2pEnabled: Boolean = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
internal fun pluginDigestHex(algorithm: String, data: String): String {
|
||||
val normalized = algorithm.uppercase()
|
||||
val digest = MessageDigest.getInstance(normalized).digest(data.encodeToByteArray())
|
||||
return digest.joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
internal fun pluginHmacHex(algorithm: String, key: String, data: String): String {
|
||||
val normalized = when (algorithm.uppercase()) {
|
||||
"SHA1" -> "HmacSHA1"
|
||||
"SHA256" -> "HmacSHA256"
|
||||
"SHA512" -> "HmacSHA512"
|
||||
"MD5" -> "HmacMD5"
|
||||
else -> error("Unsupported HMAC algorithm: $algorithm")
|
||||
}
|
||||
val mac = Mac.getInstance(normalized)
|
||||
mac.init(SecretKeySpec(key.encodeToByteArray(), normalized))
|
||||
val digest = mac.doFinal(data.encodeToByteArray())
|
||||
return digest.joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
internal fun pluginBase64Encode(data: String): String =
|
||||
Base64.getEncoder().encodeToString(data.encodeToByteArray())
|
||||
|
||||
internal fun pluginBase64Decode(data: String): String {
|
||||
val normalized = data.trim().replace("\n", "").replace("\r", "").replace(" ", "")
|
||||
val decoded = Base64.getDecoder().decode(normalized)
|
||||
return decoded.decodeToString()
|
||||
}
|
||||
|
||||
internal fun pluginUtf8ToHex(value: String): String =
|
||||
value.encodeToByteArray().joinToString(separator = "") { byte ->
|
||||
byte.toUByte().toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
internal fun pluginHexToUtf8(hex: String): String {
|
||||
val normalized = hex.trim().lowercase()
|
||||
.replace(" ", "")
|
||||
.removePrefix("0x")
|
||||
if (normalized.isEmpty()) return ""
|
||||
|
||||
val evenHex = if (normalized.length % 2 == 0) normalized else "0$normalized"
|
||||
val out = ByteArray(evenHex.length / 2)
|
||||
for (index in out.indices) {
|
||||
val part = evenHex.substring(index * 2, index * 2 + 2)
|
||||
out[index] = part.toInt(16).toByte()
|
||||
}
|
||||
return out.decodeToString()
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import com.nuvio.app.core.storage.DesktopStorage
|
||||
import java.util.Locale
|
||||
|
||||
internal object PluginStorage {
|
||||
private const val pluginsStateKey = "plugins_state"
|
||||
private val store = DesktopStorage.store("nuvio_plugins")
|
||||
|
||||
fun loadState(profileId: Int): String? =
|
||||
store.getString("${pluginsStateKey}_$profileId")
|
||||
|
||||
fun saveState(profileId: Int, payload: String) {
|
||||
store.putString("${pluginsStateKey}_$profileId", payload)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun currentPluginPlatform(): String = "desktop"
|
||||
|
||||
internal fun currentPluginPlatformTags(): Set<String> {
|
||||
val osName = System.getProperty("os.name").orEmpty().lowercase(Locale.ROOT)
|
||||
val osTag = when {
|
||||
osName.contains("mac") || osName.contains("darwin") -> "macos"
|
||||
osName.contains("win") -> "windows"
|
||||
osName.contains("linux") -> "linux"
|
||||
else -> null
|
||||
}
|
||||
return buildSet {
|
||||
add(currentPluginPlatform())
|
||||
add("jvm")
|
||||
osTag?.let(::add)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun currentEpochMillis(): Long = System.currentTimeMillis()
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.plugins_error_unavailable_build
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
actual object PluginRepository {
|
||||
private val disabledState = MutableStateFlow(PluginsUiState(pluginsEnabled = false))
|
||||
|
||||
actual val uiState: StateFlow<PluginsUiState> = disabledState.asStateFlow()
|
||||
|
||||
actual fun initialize() = Unit
|
||||
actual fun onProfileChanged(profileId: Int) = Unit
|
||||
actual fun clearLocalState() = Unit
|
||||
actual suspend fun pullFromServer(profileId: Int) = Unit
|
||||
|
||||
actual suspend fun addRepository(rawUrl: String): AddPluginRepositoryResult =
|
||||
AddPluginRepositoryResult.Error(getString(Res.string.plugins_error_unavailable_build))
|
||||
|
||||
actual fun removeRepository(manifestUrl: String) = Unit
|
||||
actual fun refreshAll() = Unit
|
||||
actual fun refreshRepository(manifestUrl: String, pushAfterRefresh: Boolean) = Unit
|
||||
actual fun toggleScraper(scraperId: String, enabled: Boolean) = Unit
|
||||
actual fun setPluginsEnabled(enabled: Boolean) = Unit
|
||||
actual fun setGroupStreamsByRepository(enabled: Boolean) = Unit
|
||||
actual fun getEnabledScrapersForType(type: String): List<PluginScraper> = emptyList()
|
||||
|
||||
actual suspend fun testScraper(scraperId: String): Result<List<PluginRuntimeResult>> =
|
||||
Result.failure(UnsupportedOperationException(getString(Res.string.plugins_error_unavailable_build)))
|
||||
|
||||
actual suspend fun executeScraper(
|
||||
scraper: PluginScraper,
|
||||
tmdbId: String,
|
||||
mediaType: String,
|
||||
season: Int?,
|
||||
episode: Int?,
|
||||
): Result<List<PluginRuntimeResult>> =
|
||||
Result.failure(UnsupportedOperationException(getString(Res.string.plugins_error_unavailable_build)))
|
||||
}
|
||||
|
|
@ -1,5 +1,14 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.nuvio.app.features.plugins.PluginsSettingsPageContent
|
||||
|
||||
internal actual fun LazyListScope.pluginsSettingsContent() = Unit
|
||||
internal actual fun LazyListScope.pluginsSettingsContent() {
|
||||
item {
|
||||
PluginsSettingsPageContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PluginRuntimeDesktopTest {
|
||||
@Test
|
||||
fun `desktop runtime executes scraper code`() = runBlocking {
|
||||
val results = PluginRuntime.executePlugin(
|
||||
code = """
|
||||
module.exports.getStreams = async function(tmdbId, mediaType) {
|
||||
return [{
|
||||
title: "Desktop stream " + tmdbId + " " + mediaType,
|
||||
url: "https://example.test/movie.mp4",
|
||||
quality: "1080p",
|
||||
provider: "Desktop Test"
|
||||
}];
|
||||
};
|
||||
""".trimIndent(),
|
||||
tmdbId = "603",
|
||||
mediaType = "movie",
|
||||
season = null,
|
||||
episode = null,
|
||||
scraperId = "desktop-runtime-test",
|
||||
)
|
||||
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("Desktop stream 603 movie", results.single().title)
|
||||
assertEquals("https://example.test/movie.mp4", results.single().url)
|
||||
assertEquals("1080p", results.single().quality)
|
||||
assertEquals("Desktop Test", results.single().provider)
|
||||
}
|
||||
}
|
||||
|
|
@ -413,11 +413,11 @@ actual object PluginRepository {
|
|||
}
|
||||
|
||||
private fun PluginManifestScraper.isSupportedOnCurrentPlatform(): Boolean {
|
||||
val platform = currentPluginPlatform().lowercase()
|
||||
val platformTags = currentPluginPlatformTags().map { it.lowercase() }.toSet()
|
||||
val supported = supportedPlatforms?.map { it.lowercase() }?.toSet().orEmpty()
|
||||
val disabled = disabledPlatforms?.map { it.lowercase() }?.toSet().orEmpty()
|
||||
if (supported.isNotEmpty() && platform !in supported) return false
|
||||
if (platform in disabled) return false
|
||||
if (supported.isNotEmpty() && platformTags.none { it in supported }) return false
|
||||
if (platformTags.any { it in disabled }) return false
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,5 +19,7 @@ internal object PluginStorage {
|
|||
|
||||
internal fun currentPluginPlatform(): String = "ios"
|
||||
|
||||
internal fun currentPluginPlatformTags(): Set<String> = setOf(currentPluginPlatform())
|
||||
|
||||
internal fun currentEpochMillis(): Long =
|
||||
(platform.Foundation.NSDate().timeIntervalSince1970 * 1000.0).toLong()
|
||||
|
|
|
|||
Loading…
Reference in a new issue