AquaStream/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt

167 lines
5.9 KiB
Kotlin

package com.lagradost.cloudstream3.plugins
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.apmapIndexed
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.BufferedInputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
/**
* Comes with the app, always available in the app, non removable.
* */
val PREBUILT_REPOSITORIES = arrayOf(
// TODO FIX
RepositoryData(
"Testing repository",
"https://raw.githubusercontent.com/recloudstream/cs-repos/master/test.json"
)
)
data class Repository(
@JsonProperty("name") val name: String,
@JsonProperty("description") val description: String?,
@JsonProperty("manifestVersion") val manifestVersion: Int,
@JsonProperty("pluginLists") val pluginLists: List<String>
)
/**
* Status int as the following:
* 0: Down
* 1: Ok
* 2: Slow
* 3: Beta only
* */
data class SitePlugin(
// Url to the .cs3 file
@JsonProperty("url") val url: String,
// Status to remotely disable the provider
@JsonProperty("status") val status: Int,
// Integer over 0, any change of this will trigger an auto update
@JsonProperty("version") val version: Int,
// Unused currently, used to make the api backwards compatible?
// Set to 1
@JsonProperty("apiVersion") val apiVersion: Int,
// Name to be shown in app
@JsonProperty("name") val name: String,
// Name to be referenced internally. Separate to make name and url changes possible
@JsonProperty("internalName") val internalName: String,
@JsonProperty("authors") val authors: List<String>,
@JsonProperty("description") val description: String?,
// Might be used to go directly to the plugin repo in the future
@JsonProperty("repositoryUrl") val repositoryUrl: String?,
// These types are yet to be mapped and used, ignore for now
// @JsonProperty("tvTypes") val tvTypes: List<String>?,
// @JsonProperty("language") val language: String?,
// @JsonProperty("iconUrl") val iconUrl: String?,
// Set to true to get an 18+ symbol next to the plugin
@JsonProperty("adult") val adult: Boolean?,
)
object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions"
suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall {
// Take manifestVersion and such into account later
app.get(url).parsedSafe()
}
}
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
// Take manifestVersion and such into account later
val response = app.get(pluginUrls)
// Normal parsed function not working?
// return response.parsedSafe()
return tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
}
/**
* Gets all plugins from repositories and pairs them with the repository url
* */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
val repo = parseRepository(repositoryUrl) ?: return null
return repo.pluginLists.apmapIndexed { index, url ->
parsePlugins(url).map {
repo.pluginLists[index] to it
}
}.flatten()
}
suspend fun downloadPluginToFile(
context: Context,
pluginUrl: String,
fileName: String,
folder: String
): File? {
return suspendSafeApiCall {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
if (!extensionsDir.exists())
extensionsDir.mkdirs()
val newDir = File(extensionsDir, folder)
newDir.mkdirs()
val newFile = File(newDir, "${fileName}.cs3")
// Overwrite if exists
if (newFile.exists()) {
newFile.delete()
}
newFile.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body
write(body.byteStream(), newFile.outputStream())
newFile
}
}
// Don't want to read before we write in another thread
private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock {
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
// No duplicates
setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
}
}
/**
* Also deletes downloaded repository plugins
* */
suspend fun removeRepository(context: Context, repository: RepositoryData) {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
repoLock.withLock {
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
// No duplicates
val newRepos = currentRepos.filter { it.url != repository.url }
setKey(REPOSITORIES_KEY, newRepos)
}
File(
extensionsDir,
getPluginSanitizedFileName(repository.url)
).delete()
}
private fun write(stream: InputStream, output: OutputStream) {
val input = BufferedInputStream(stream)
val dataBuffer = ByteArray(512)
var readBytes: Int
while (input.read(dataBuffer).also { readBytes = it } != -1) {
output.write(dataBuffer, 0, readBytes)
}
}
}