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 ) /** * 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, @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?, // @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 { // Take manifestVersion and such into account later val response = app.get(pluginUrls) // Normal parsed function not working? // return response.parsedSafe() return tryParseJson>(response.text)?.toList() ?: emptyList() } /** * Gets all plugins from repositories and pairs them with the repository url * */ suspend fun getRepoPlugins(repositoryUrl: String): List>? { 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>(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>(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) } } }