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

213 lines
7.9 KiB
Kotlin
Raw Normal View History

2022-08-06 18:27:56 +00:00
package com.lagradost.cloudstream3.plugins
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.context
2022-08-06 23:43:39 +00:00
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
2022-10-31 00:16:15 +00:00
import com.lagradost.cloudstream3.amap
2022-08-06 18:27:56 +00:00
import com.lagradost.cloudstream3.app
2022-08-20 17:39:04 +00:00
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
2022-08-06 18:27:56 +00:00
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
2022-08-08 18:14:57 +00:00
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
2022-08-06 23:43:39 +00:00
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
2022-08-06 18:27:56 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
2022-08-06 23:43:39 +00:00
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
2022-08-06 18:27:56 +00:00
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.
* */
2022-08-06 18:27:56 +00:00
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
* */
2022-08-06 18:27:56 +00:00
data class SitePlugin(
// Url to the .cs3 file
2022-08-06 18:27:56 +00:00
@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
2022-08-06 18:27:56 +00:00
@JsonProperty("version") val version: Int,
// Unused currently, used to make the api backwards compatible?
// Set to 1
2022-08-06 18:27:56 +00:00
@JsonProperty("apiVersion") val apiVersion: Int,
// Name to be shown in app
2022-08-06 18:27:56 +00:00
@JsonProperty("name") val name: String,
// Name to be referenced internally. Separate to make name and url changes possible
@JsonProperty("internalName") val internalName: String,
2022-08-06 18:27:56 +00:00
@JsonProperty("authors") val authors: List<String>,
@JsonProperty("description") val description: String?,
// Might be used to go directly to the plugin repo in the future
2022-08-06 18:27:56 +00:00
@JsonProperty("repositoryUrl") val repositoryUrl: String?,
// These types are yet to be mapped and used, ignore for now
2022-08-13 21:49:16 +00:00
@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?,
2022-08-06 18:27:56 +00:00
)
2022-08-06 23:43:39 +00:00
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"
}
2022-08-06 23:43:39 +00:00
suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
2022-10-31 11:49:00 +00:00
} 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 {
2023-04-21 11:54:58 +00:00
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
}
2022-08-07 00:48:49 +00:00
suspend fun parseRepository(url: String): Repository? {
2022-08-06 18:27:56 +00:00
return suspendSafeApiCall {
// Take manifestVersion and such into account later
app.get(convertRawGitUrl(url)).parsedSafe()
2022-08-06 18:27:56 +00:00
}
}
2022-08-07 00:48:49 +00:00
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
2022-08-06 18:27:56 +00:00
// Take manifestVersion and such into account later
2022-08-20 17:39:04 +00:00
return try {
val response = app.get(convertRawGitUrl(pluginUrls))
2022-08-20 17:39:04 +00:00
// Normal parsed function not working?
// return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
2022-09-23 12:20:53 +00:00
} catch (t: Throwable) {
2022-08-25 01:59:20 +00:00
logError(t)
2022-08-20 17:39:04 +00:00
emptyList()
}
2022-08-06 18:27:56 +00:00
}
/**
* Gets all plugins from repositories and pairs them with the repository url
* */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
2022-08-06 18:27:56 +00:00
val repo = parseRepository(repositoryUrl) ?: return null
2022-10-31 00:16:15 +00:00
return repo.pluginLists.amap { url ->
parsePlugins(url).map {
2022-08-11 22:36:19 +00:00
repositoryUrl to it
}
}.flatten()
2022-08-06 18:27:56 +00:00
}
2022-09-23 12:20:53 +00:00
suspend fun downloadPluginToFile(
pluginUrl: String,
file: File
): File? {
return suspendSafeApiCall {
file.mkdirs()
// Overwrite if exists
if (file.exists()) {
file.delete()
}
2022-09-23 12:20:53 +00:00
file.createNewFile()
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
2022-09-23 12:20:53 +00:00
write(body.byteStream(), file.outputStream())
file
}
}
2022-08-15 01:31:33 +00:00
fun getRepositories(): Array<RepositoryData> {
return getKey(REPOSITORIES_KEY) ?: emptyArray()
}
2022-08-06 23:43:39 +00:00
// Don't want to read before we write in another thread
private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock {
2022-08-15 01:31:33 +00:00
val currentRepos = getRepositories()
// No duplicates
setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
}
2022-08-07 00:48:49 +00:00
}
2022-08-08 18:14:57 +00:00
/**
* 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)
}
2022-08-08 18:14:57 +00:00
2022-08-11 22:36:19 +00:00
val file = File(
2022-08-08 18:14:57 +00:00
extensionsDir,
getPluginSanitizedFileName(repository.url)
2022-08-11 22:36:19 +00:00
)
// 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)
2022-08-06 18:27:56 +00:00
}
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)
}
}
2022-10-31 11:49:00 +00:00
}