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

213 lines
7.9 KiB
Kotlin

package com.lagradost.cloudstream3.plugins
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
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.
* */
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?,
// Automatically generated by the gradle plugin
@JsonProperty("fileSize") val fileSize: Long?,
)
object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions"
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
}
val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String {
if (getKey<Boolean>(context!!.getString(R.string.jsdelivr_proxy_key)) != true) return url
val match = GH_REGEX.find(url) ?: return url
val (user, repo, rest) = match.destructured
return "https://cdn.jsdelivr.net/gh/$user/$repo@$rest"
}
suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
return@let if (!it.contains("^https?://".toRegex()))
"https://${it}"
else fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall {
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
it2.headers["Location"]?.let { url ->
if (url.startsWith("https://cutt.ly/404")) return@suspendSafeApiCall null
if (url.removeSuffix("/") == "https://cutt.ly") return@suspendSafeApiCall null
return@suspendSafeApiCall url
}
}
}
} else null
}
suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall {
// Take manifestVersion and such into account later
app.get(convertRawGitUrl(url)).parsedSafe()
}
}
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
// Take manifestVersion and such into account later
return try {
val response = app.get(convertRawGitUrl(pluginUrls))
// Normal parsed function not working?
// return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
} catch (t: Throwable) {
logError(t)
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.amap { url ->
parsePlugins(url).map {
repositoryUrl to it
}
}.flatten()
}
suspend fun downloadPluginToFile(
pluginUrl: String,
file: File
): File? {
return suspendSafeApiCall {
file.mkdirs()
// Overwrite if exists
if (file.exists()) {
file.delete()
}
file.createNewFile()
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
write(body.byteStream(), file.outputStream())
file
}
}
fun getRepositories(): Array<RepositoryData> {
return getKey(REPOSITORIES_KEY) ?: emptyArray()
}
// Don't want to read before we write in another thread
private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock {
val currentRepos = getRepositories()
// 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)
}
val file = File(
extensionsDir,
getPluginSanitizedFileName(repository.url)
)
// Unload all plugins, not using deletePlugin since we
// delete all data and files in deleteRepositoryData
normalSafeApiCall {
file.listFiles { plugin: File ->
unloadPlugin(plugin.absolutePath)
false
}
}
PluginManager.deleteRepositoryData(file.absolutePath)
}
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)
}
}
}