mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 08:39:24 +00:00
Merge 141d74a814 into 2fda777249
This commit is contained in:
commit
5d1abe9eb5
4 changed files with 189 additions and 23 deletions
|
|
@ -41,6 +41,9 @@ data class PluginRepositoryItem(
|
|||
val lastUpdated: Long = 0L,
|
||||
val isRefreshing: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val serverUrl: String? = null,
|
||||
val serverRepoType: String? = null,
|
||||
val serverEnabled: Boolean? = null,
|
||||
)
|
||||
|
||||
data class PluginScraper(
|
||||
|
|
@ -117,6 +120,9 @@ internal data class StoredPluginRepository(
|
|||
val version: String? = null,
|
||||
val scraperCount: Int = 0,
|
||||
val lastUpdated: Long = 0L,
|
||||
val serverUrl: String? = null,
|
||||
val serverRepoType: String? = null,
|
||||
val serverEnabled: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
internal data class PluginPushEntry(
|
||||
val url: String,
|
||||
val name: String,
|
||||
val enabled: Boolean,
|
||||
val repoType: String?,
|
||||
val sortOrder: Int,
|
||||
)
|
||||
|
||||
internal fun buildPluginPushEntries(repositories: List<PluginRepositoryItem>): List<PluginPushEntry> =
|
||||
repositories.mapIndexed { index, repo ->
|
||||
PluginPushEntry(
|
||||
url = repo.serverUrl?.takeIf { it.isNotBlank() } ?: repo.manifestUrl,
|
||||
name = repo.name,
|
||||
enabled = repo.serverEnabled ?: true,
|
||||
repoType = repo.serverRepoType?.takeIf { it.isNotBlank() },
|
||||
sortOrder = index,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun shouldPreserveLocalPluginRepositories(
|
||||
remoteUrls: List<String>,
|
||||
localRepositories: List<PluginRepositoryItem>,
|
||||
): Boolean = remoteUrls.isEmpty() && localRepositories.isNotEmpty()
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.nuvio.app.features.plugins
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PluginSyncPlanTest {
|
||||
|
||||
private fun repo(
|
||||
manifestUrl: String,
|
||||
name: String = "Repo",
|
||||
serverUrl: String? = null,
|
||||
serverRepoType: String? = null,
|
||||
serverEnabled: Boolean? = null,
|
||||
) = PluginRepositoryItem(
|
||||
manifestUrl = manifestUrl,
|
||||
name = name,
|
||||
serverUrl = serverUrl,
|
||||
serverRepoType = serverRepoType,
|
||||
serverEnabled = serverEnabled,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun pushEntriesPreserveOriginalServerUrlAndRepoType() {
|
||||
val cloudStreamRepo = repo(
|
||||
manifestUrl = "https://example.com/cs/repo.json/manifest.json",
|
||||
name = "CS Repo",
|
||||
serverUrl = "https://example.com/cs/repo.json",
|
||||
serverRepoType = "CLOUDSTREAM",
|
||||
serverEnabled = false,
|
||||
)
|
||||
|
||||
val entries = buildPluginPushEntries(listOf(cloudStreamRepo))
|
||||
|
||||
assertEquals(1, entries.size)
|
||||
assertEquals("https://example.com/cs/repo.json", entries[0].url)
|
||||
assertEquals("CLOUDSTREAM", entries[0].repoType)
|
||||
assertEquals(false, entries[0].enabled)
|
||||
assertEquals(0, entries[0].sortOrder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushEntriesFallBackToManifestUrlForLocallyAddedRepos() {
|
||||
val localRepo = repo(
|
||||
manifestUrl = "https://example.com/nuvio/manifest.json",
|
||||
name = "Local Repo",
|
||||
)
|
||||
|
||||
val entries = buildPluginPushEntries(listOf(localRepo))
|
||||
|
||||
assertEquals("https://example.com/nuvio/manifest.json", entries[0].url)
|
||||
assertNull(entries[0].repoType)
|
||||
assertTrue(entries[0].enabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushEntriesIgnoreBlankServerMetadata() {
|
||||
val blankMetadataRepo = repo(
|
||||
manifestUrl = "https://example.com/nuvio/manifest.json",
|
||||
serverUrl = " ",
|
||||
serverRepoType = "",
|
||||
)
|
||||
|
||||
val entries = buildPluginPushEntries(listOf(blankMetadataRepo))
|
||||
|
||||
assertEquals("https://example.com/nuvio/manifest.json", entries[0].url)
|
||||
assertNull(entries[0].repoType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pushEntriesAssignSortOrderByPosition() {
|
||||
val entries = buildPluginPushEntries(
|
||||
listOf(
|
||||
repo(manifestUrl = "https://a.example/manifest.json"),
|
||||
repo(manifestUrl = "https://b.example/manifest.json"),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(listOf(0, 1), entries.map { it.sortOrder })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preservesLocalWhenRemoteEmptyAndLocalHasRepositories() {
|
||||
assertTrue(
|
||||
shouldPreserveLocalPluginRepositories(
|
||||
remoteUrls = emptyList(),
|
||||
localRepositories = listOf(repo(manifestUrl = "https://a.example/manifest.json")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotPreserveLocalWhenRemoteHasRepositories() {
|
||||
assertFalse(
|
||||
shouldPreserveLocalPluginRepositories(
|
||||
remoteUrls = listOf("https://a.example/manifest.json"),
|
||||
localRepositories = listOf(repo(manifestUrl = "https://b.example/manifest.json")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesNotPreserveLocalWhenLocalIsEmpty() {
|
||||
assertFalse(
|
||||
shouldPreserveLocalPluginRepositories(
|
||||
remoteUrls = emptyList(),
|
||||
localRepositories = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ private data class PluginRow(
|
|||
val name: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
@SerialName("repo_type") val repoType: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -50,6 +51,7 @@ private data class PluginPushItem(
|
|||
val name: String = "",
|
||||
val enabled: Boolean = true,
|
||||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
@SerialName("repo_type") val repoType: String? = null,
|
||||
)
|
||||
|
||||
actual object PluginRepository {
|
||||
|
|
@ -61,7 +63,6 @@ actual object PluginRepository {
|
|||
actual val uiState: StateFlow<PluginsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var initialized = false
|
||||
private var pulledFromServer = false
|
||||
private var currentProfileId = 1
|
||||
private val activeRefreshJobs = mutableMapOf<String, Job>()
|
||||
|
||||
|
|
@ -83,7 +84,6 @@ actual object PluginRepository {
|
|||
cancelActiveRefreshes()
|
||||
currentProfileId = effectiveProfileId
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
_uiState.value = PluginsUiState()
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,6 @@ actual object PluginRepository {
|
|||
cancelActiveRefreshes()
|
||||
currentProfileId = 1
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
_uiState.value = PluginsUiState()
|
||||
}
|
||||
|
||||
|
|
@ -107,25 +106,38 @@ actual object PluginRepository {
|
|||
}
|
||||
.decodeList<PluginRow>()
|
||||
|
||||
val urls = dedupeManifestUrls(rows.map { it.url })
|
||||
if (urls.isEmpty() && !pulledFromServer) {
|
||||
val localUrls = _uiState.value.repositories.map { it.manifestUrl }
|
||||
if (localUrls.isNotEmpty()) {
|
||||
initialize()
|
||||
pulledFromServer = true
|
||||
pushToServer()
|
||||
return
|
||||
val rowsByUrl = linkedMapOf<String, PluginRow>()
|
||||
rows.forEach { row ->
|
||||
val manifestUrl = ensureManifestSuffix(row.url)
|
||||
if (!rowsByUrl.containsKey(manifestUrl)) {
|
||||
rowsByUrl[manifestUrl] = row
|
||||
}
|
||||
}
|
||||
val urls = rowsByUrl.keys.toList()
|
||||
|
||||
if (shouldPreserveLocalPluginRepositories(urls, _uiState.value.repositories)) {
|
||||
log.w { "pullFromServer — remote empty while local has repositories; preserving local" }
|
||||
_uiState.value.repositories.forEach { repo ->
|
||||
refreshRepository(repo.manifestUrl, pushAfterRefresh = false)
|
||||
}
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
|
||||
val existingReposByUrl = _uiState.value.repositories.associateBy { it.manifestUrl }
|
||||
val nextRepos = urls.map { url ->
|
||||
existingReposByUrl[url]?.copy(isRefreshing = true, errorMessage = null)
|
||||
val row = rowsByUrl.getValue(url)
|
||||
val base = existingReposByUrl[url]?.copy(isRefreshing = true, errorMessage = null)
|
||||
?: PluginRepositoryItem(
|
||||
manifestUrl = url,
|
||||
name = url.substringBefore("?").substringAfterLast('/'),
|
||||
isRefreshing = true,
|
||||
)
|
||||
base.copy(
|
||||
serverUrl = row.url,
|
||||
serverRepoType = row.repoType,
|
||||
serverEnabled = row.enabled,
|
||||
)
|
||||
}
|
||||
val nextScrapers = _uiState.value.scrapers.filter { scraper ->
|
||||
urls.contains(scraper.repositoryUrl)
|
||||
|
|
@ -143,7 +155,6 @@ actual object PluginRepository {
|
|||
refreshRepository(url, pushAfterRefresh = false)
|
||||
}
|
||||
|
||||
pulledFromServer = true
|
||||
initialized = true
|
||||
}.onFailure { error ->
|
||||
log.e(error) { "pullFromServer failed" }
|
||||
|
|
@ -229,7 +240,15 @@ actual object PluginRepository {
|
|||
result.fold(
|
||||
onSuccess = { (repo, scrapers) ->
|
||||
val updatedRepos = state.repositories.map { existing ->
|
||||
if (existing.manifestUrl == manifestUrl) repo else existing
|
||||
if (existing.manifestUrl == manifestUrl) {
|
||||
repo.copy(
|
||||
serverUrl = existing.serverUrl,
|
||||
serverRepoType = existing.serverRepoType,
|
||||
serverEnabled = existing.serverEnabled,
|
||||
)
|
||||
} else {
|
||||
existing
|
||||
}
|
||||
}
|
||||
state.copy(
|
||||
repositories = updatedRepos,
|
||||
|
|
@ -439,12 +458,13 @@ actual object PluginRepository {
|
|||
private fun pushToServer() {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
val repos = _uiState.value.repositories.mapIndexed { index, repo ->
|
||||
val repos = buildPluginPushEntries(_uiState.value.repositories).map { entry ->
|
||||
PluginPushItem(
|
||||
url = repo.manifestUrl,
|
||||
name = repo.name,
|
||||
enabled = true,
|
||||
sortOrder = index,
|
||||
url = entry.url,
|
||||
name = entry.name,
|
||||
enabled = entry.enabled,
|
||||
sortOrder = entry.sortOrder,
|
||||
repoType = entry.repoType,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -472,6 +492,9 @@ actual object PluginRepository {
|
|||
version = repo.version,
|
||||
scraperCount = repo.scraperCount,
|
||||
lastUpdated = repo.lastUpdated,
|
||||
serverUrl = repo.serverUrl,
|
||||
serverRepoType = repo.serverRepoType,
|
||||
serverEnabled = repo.serverEnabled,
|
||||
)
|
||||
},
|
||||
scrapers = state.scrapers.map { scraper ->
|
||||
|
|
@ -513,7 +536,6 @@ actual object PluginRepository {
|
|||
|
||||
if (currentProfileId != profileId) {
|
||||
cancelActiveRefreshes()
|
||||
pulledFromServer = false
|
||||
}
|
||||
|
||||
currentProfileId = profileId
|
||||
|
|
@ -537,6 +559,9 @@ actual object PluginRepository {
|
|||
lastUpdated = it.lastUpdated,
|
||||
isRefreshing = false,
|
||||
errorMessage = null,
|
||||
serverUrl = it.serverUrl,
|
||||
serverRepoType = it.serverRepoType,
|
||||
serverEnabled = it.serverEnabled,
|
||||
)
|
||||
}
|
||||
?: emptyList(),
|
||||
|
|
@ -563,9 +588,6 @@ actual object PluginRepository {
|
|||
)
|
||||
}
|
||||
|
||||
private fun dedupeManifestUrls(urls: List<String>): List<String> =
|
||||
urls.map(::ensureManifestSuffix).distinct()
|
||||
|
||||
private fun ensureManifestSuffix(url: String): String {
|
||||
val path = url.substringBefore("?").trimEnd('/')
|
||||
val query = url.substringAfter("?", "")
|
||||
|
|
|
|||
Loading…
Reference in a new issue