2022-08-06 15:48:00 +00:00
|
|
|
package com.lagradost.cloudstream3.plugins
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import dalvik.system.PathClassLoader
|
|
|
|
import com.google.gson.Gson
|
|
|
|
import android.content.res.AssetManager
|
|
|
|
import android.content.res.Resources
|
|
|
|
import android.os.Environment
|
2022-08-07 07:28:21 +00:00
|
|
|
import android.widget.Toast
|
|
|
|
import android.app.Activity
|
2022-08-07 21:11:13 +00:00
|
|
|
import android.util.Log
|
2022-08-06 23:43:39 +00:00
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|
|
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
|
|
|
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
|
2022-08-07 07:28:21 +00:00
|
|
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
2022-08-08 18:52:03 +00:00
|
|
|
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
2022-08-07 07:28:21 +00:00
|
|
|
import com.lagradost.cloudstream3.R
|
2022-08-07 16:01:32 +00:00
|
|
|
import com.lagradost.cloudstream3.apmap
|
|
|
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
|
|
|
|
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
|
|
|
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
|
|
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
2022-08-06 23:43:39 +00:00
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
2022-08-06 15:48:00 +00:00
|
|
|
import java.io.File
|
|
|
|
import java.io.InputStreamReader
|
|
|
|
import java.util.*
|
|
|
|
|
2022-08-06 23:43:39 +00:00
|
|
|
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
|
|
|
|
const val PLUGINS_KEY = "PLUGINS_KEY"
|
|
|
|
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
|
|
|
|
2022-08-07 16:01:32 +00:00
|
|
|
|
|
|
|
// Data class for internal storage
|
2022-08-06 23:43:39 +00:00
|
|
|
data class PluginData(
|
2022-08-07 16:01:32 +00:00
|
|
|
@JsonProperty("internalName") val internalName: String,
|
2022-08-06 23:43:39 +00:00
|
|
|
@JsonProperty("url") val url: String?,
|
|
|
|
@JsonProperty("isOnline") val isOnline: Boolean,
|
|
|
|
@JsonProperty("filePath") val filePath: String,
|
2022-08-07 16:01:32 +00:00
|
|
|
@JsonProperty("version") val version: Int,
|
2022-08-06 23:43:39 +00:00
|
|
|
)
|
|
|
|
|
2022-08-07 16:01:32 +00:00
|
|
|
// This is used as a placeholder / not set version
|
|
|
|
const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE
|
|
|
|
|
|
|
|
// This always updates
|
|
|
|
const val PLUGIN_VERSION_ALWAYS_UPDATE = -1
|
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
object PluginManager {
|
2022-08-06 23:43:39 +00:00
|
|
|
// Prevent multiple writes at once
|
|
|
|
val lock = Mutex()
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
const val TAG = "PluginManager"
|
|
|
|
|
2022-08-06 23:43:39 +00:00
|
|
|
/**
|
|
|
|
* Store data about the plugin for fetching later
|
|
|
|
* */
|
2022-08-07 21:11:13 +00:00
|
|
|
private suspend fun setPluginData(data: PluginData) {
|
|
|
|
lock.withLock {
|
|
|
|
if (data.isOnline) {
|
|
|
|
val plugins = getPluginsOnline()
|
|
|
|
setKey(PLUGINS_KEY, plugins + data)
|
|
|
|
} else {
|
|
|
|
val plugins = getPluginsLocal()
|
|
|
|
setKey(PLUGINS_KEY_LOCAL, plugins + data)
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
private suspend fun deletePluginData(data: PluginData) {
|
|
|
|
lock.withLock {
|
|
|
|
if (data.isOnline) {
|
|
|
|
val plugins = getPluginsOnline().filter { it.url != data.url }
|
|
|
|
setKey(PLUGINS_KEY, plugins)
|
|
|
|
} else {
|
|
|
|
val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
|
|
|
|
setKey(PLUGINS_KEY_LOCAL, plugins + data)
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getPluginsOnline(): Array<PluginData> {
|
|
|
|
return getKey(PLUGINS_KEY) ?: emptyArray()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getPluginsLocal(): Array<PluginData> {
|
|
|
|
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
|
|
|
|
}
|
|
|
|
|
|
|
|
private val LOCAL_PLUGINS_PATH =
|
2022-08-06 15:48:00 +00:00
|
|
|
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
|
2022-08-06 23:43:39 +00:00
|
|
|
|
2022-08-07 16:01:32 +00:00
|
|
|
// Maps filepath to plugin
|
2022-08-06 15:48:00 +00:00
|
|
|
private val plugins: MutableMap<String, Plugin> =
|
|
|
|
LinkedHashMap<String, Plugin>()
|
2022-08-07 16:01:32 +00:00
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
|
|
|
HashMap<PathClassLoader, Plugin>()
|
2022-08-07 16:01:32 +00:00
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
private val failedToLoad: MutableMap<File, Any> = LinkedHashMap()
|
2022-08-07 16:01:32 +00:00
|
|
|
private var loadedLocalPlugins = false
|
2022-08-06 15:48:00 +00:00
|
|
|
private val gson = Gson()
|
2022-08-06 23:43:39 +00:00
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
private suspend fun maybeLoadPlugin(activity: Activity, file: File) {
|
2022-08-06 23:43:39 +00:00
|
|
|
val name = file.name
|
|
|
|
if (file.extension == "zip" || file.extension == "cs3") {
|
2022-08-07 16:01:32 +00:00
|
|
|
loadPlugin(
|
2022-08-07 21:11:13 +00:00
|
|
|
activity,
|
2022-08-07 16:01:32 +00:00
|
|
|
file,
|
|
|
|
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-08 18:52:03 +00:00
|
|
|
|
|
|
|
// Helper class for updateAllOnlinePluginsAndLoadThem
|
|
|
|
private data class OnlinePluginData(
|
|
|
|
val savedData: PluginData,
|
|
|
|
val onlineData: Pair<String, SitePlugin>,
|
|
|
|
) {
|
|
|
|
val isOutdated =
|
|
|
|
onlineData.second.apiVersion != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
|
|
|
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
|
|
|
|
}
|
|
|
|
|
2022-08-07 16:01:32 +00:00
|
|
|
/**
|
|
|
|
* Needs to be run before other plugin loading because plugin loading can not be overwritten
|
2022-08-08 18:52:03 +00:00
|
|
|
* 1. Gets all online data about the downloaded plugins
|
|
|
|
* 2. If disabled do nothing
|
|
|
|
* 3. If outdated download and load the plugin
|
|
|
|
* 4. Else load the plugin normally
|
2022-08-07 16:01:32 +00:00
|
|
|
**/
|
2022-08-08 18:52:03 +00:00
|
|
|
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
|
2022-08-07 16:01:32 +00:00
|
|
|
val urls = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
|
|
|
|
|
|
|
|
val onlinePlugins = urls.toList().apmap {
|
|
|
|
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
2022-08-08 18:52:03 +00:00
|
|
|
}.flatten().distinctBy { it.second.url }
|
2022-08-07 16:01:32 +00:00
|
|
|
|
|
|
|
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
|
|
|
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
|
|
|
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
2022-08-08 18:52:03 +00:00
|
|
|
.map { onlineData ->
|
|
|
|
OnlinePluginData(savedData, onlineData)
|
2022-08-07 16:01:32 +00:00
|
|
|
}
|
2022-08-08 18:52:03 +00:00
|
|
|
}.flatten().distinctBy { it.onlineData.second.url }
|
2022-08-07 16:01:32 +00:00
|
|
|
|
2022-08-08 18:52:03 +00:00
|
|
|
Log.i(TAG, "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}")
|
2022-08-07 16:01:32 +00:00
|
|
|
|
|
|
|
outdatedPlugins.apmap {
|
2022-08-08 18:52:03 +00:00
|
|
|
if (it.isDisabled) {
|
|
|
|
return@apmap
|
|
|
|
} else if (it.isOutdated) {
|
|
|
|
downloadAndLoadPlugin(
|
|
|
|
activity,
|
|
|
|
it.onlineData.second.url,
|
|
|
|
it.savedData.internalName,
|
|
|
|
it.onlineData.first
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
loadPlugin(
|
|
|
|
activity,
|
|
|
|
File(it.savedData.filePath),
|
|
|
|
it.savedData
|
|
|
|
)
|
|
|
|
}
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
2022-08-07 16:01:32 +00:00
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
Log.i(TAG, "Plugin update done!")
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
|
|
|
|
2022-08-08 18:52:03 +00:00
|
|
|
/**
|
|
|
|
* Use updateAllOnlinePluginsAndLoadThem
|
|
|
|
* */
|
2022-08-07 21:11:13 +00:00
|
|
|
fun loadAllOnlinePlugins(activity: Activity) {
|
|
|
|
File(activity.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
|
|
|
|
?.apmap { file ->
|
|
|
|
maybeLoadPlugin(activity, file)
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
fun loadAllLocalPlugins(activity: Activity) {
|
2022-08-06 23:43:39 +00:00
|
|
|
val dir = File(LOCAL_PLUGINS_PATH)
|
|
|
|
removeKey(PLUGINS_KEY_LOCAL)
|
|
|
|
|
2022-08-04 10:51:11 +00:00
|
|
|
if (!dir.exists()) {
|
2022-08-06 15:48:00 +00:00
|
|
|
val res = dir.mkdirs()
|
2022-08-04 10:51:11 +00:00
|
|
|
if (!res) {
|
|
|
|
//logger.error("Failed to create directories!", null);
|
2022-08-06 15:48:00 +00:00
|
|
|
return
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-06 23:43:39 +00:00
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
val sortedPlugins = dir.listFiles()
|
2022-08-04 10:51:11 +00:00
|
|
|
// Always sort plugins alphabetically for reproducible results
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
sortedPlugins?.sortedBy { it.name }?.apmap { file ->
|
|
|
|
maybeLoadPlugin(activity, file)
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
2022-08-06 15:48:00 +00:00
|
|
|
|
2022-08-06 23:43:39 +00:00
|
|
|
loadedLocalPlugins = true
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
|
|
|
|
2022-08-06 23:43:39 +00:00
|
|
|
/**
|
|
|
|
* @return True if successful, false if not
|
|
|
|
* */
|
2022-08-07 21:11:13 +00:00
|
|
|
private suspend fun loadPlugin(activity: Activity, file: File, data: PluginData): Boolean {
|
2022-08-06 15:48:00 +00:00
|
|
|
val fileName = file.nameWithoutExtension
|
2022-08-07 16:01:32 +00:00
|
|
|
val filePath = file.absolutePath
|
2022-08-07 21:11:13 +00:00
|
|
|
Log.i(TAG, "Loading plugin: $data")
|
2022-08-06 23:43:39 +00:00
|
|
|
|
2022-08-04 10:51:11 +00:00
|
|
|
//logger.info("Loading plugin: " + fileName);
|
2022-08-06 23:43:39 +00:00
|
|
|
return try {
|
2022-08-07 21:11:13 +00:00
|
|
|
val loader = PathClassLoader(filePath, activity.classLoader)
|
2022-08-06 15:48:00 +00:00
|
|
|
var manifest: Plugin.Manifest
|
|
|
|
loader.getResourceAsStream("manifest.json").use { stream ->
|
2022-08-04 10:51:11 +00:00
|
|
|
if (stream == null) {
|
2022-08-06 15:48:00 +00:00
|
|
|
failedToLoad[file] = "No manifest found"
|
2022-08-04 10:51:11 +00:00
|
|
|
//logger.error("Failed to load plugin " + fileName + ": No manifest found", null);
|
2022-08-06 23:43:39 +00:00
|
|
|
return false
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
2022-08-06 15:48:00 +00:00
|
|
|
InputStreamReader(stream).use { reader ->
|
|
|
|
manifest = gson.fromJson(
|
|
|
|
reader,
|
|
|
|
Plugin.Manifest::class.java
|
|
|
|
)
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-07 16:01:32 +00:00
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
val name: String = manifest.name ?: "NO NAME"
|
2022-08-07 16:01:32 +00:00
|
|
|
val version: Int = manifest.pluginVersion ?: PLUGIN_VERSION_NOT_SET
|
2022-08-06 15:48:00 +00:00
|
|
|
val pluginClass: Class<*> =
|
|
|
|
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
|
|
|
|
val pluginInstance: Plugin =
|
|
|
|
pluginClass.newInstance() as Plugin
|
2022-08-07 16:01:32 +00:00
|
|
|
|
|
|
|
// Sets with the proper version
|
|
|
|
setPluginData(data.copy(version = version))
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
if (plugins.containsKey(filePath)) {
|
|
|
|
Log.i(TAG, "Plugin with name $name already exists")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-08-06 15:48:00 +00:00
|
|
|
pluginInstance.__filename = fileName
|
2022-08-04 10:51:11 +00:00
|
|
|
if (pluginInstance.needsResources) {
|
|
|
|
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
|
2022-08-06 15:48:00 +00:00
|
|
|
val assets = AssetManager::class.java.newInstance()
|
|
|
|
val addAssetPath =
|
|
|
|
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
|
|
|
|
addAssetPath.invoke(assets, file.absolutePath)
|
|
|
|
pluginInstance.resources = Resources(
|
|
|
|
assets,
|
2022-08-07 21:11:13 +00:00
|
|
|
activity.resources.displayMetrics,
|
|
|
|
activity.resources.configuration
|
2022-08-06 15:48:00 +00:00
|
|
|
)
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
2022-08-07 16:01:32 +00:00
|
|
|
plugins[filePath] = pluginInstance
|
2022-08-06 15:48:00 +00:00
|
|
|
classLoaders[loader] = pluginInstance
|
2022-08-07 21:11:13 +00:00
|
|
|
pluginInstance.load(activity)
|
|
|
|
Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
|
2022-08-06 23:43:39 +00:00
|
|
|
true
|
2022-08-06 15:48:00 +00:00
|
|
|
} catch (e: Throwable) {
|
|
|
|
failedToLoad[file] = e
|
|
|
|
e.printStackTrace()
|
2022-08-07 07:28:21 +00:00
|
|
|
showToast(
|
2022-08-07 21:11:13 +00:00
|
|
|
activity,
|
|
|
|
activity.getString(R.string.plugin_load_fail).format(fileName),
|
2022-08-07 07:28:21 +00:00
|
|
|
Toast.LENGTH_LONG
|
|
|
|
)
|
2022-08-06 23:43:39 +00:00
|
|
|
false
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-06 23:43:39 +00:00
|
|
|
|
2022-08-08 18:14:57 +00:00
|
|
|
/**
|
|
|
|
* Spits out a unique and safe filename based on name.
|
|
|
|
* Used for repo folders (using repo url) and plugin file names (using internalName)
|
|
|
|
* */
|
|
|
|
fun getPluginSanitizedFileName(name: String): String {
|
|
|
|
return sanitizeFilename(
|
|
|
|
name,
|
|
|
|
true
|
|
|
|
) + "." + name.hashCode()
|
|
|
|
}
|
|
|
|
|
2022-08-07 16:01:32 +00:00
|
|
|
suspend fun downloadAndLoadPlugin(
|
2022-08-07 21:11:13 +00:00
|
|
|
activity: Activity,
|
2022-08-07 16:01:32 +00:00
|
|
|
pluginUrl: String,
|
|
|
|
internalName: String,
|
|
|
|
repositoryUrl: String
|
|
|
|
): Boolean {
|
2022-08-08 18:14:57 +00:00
|
|
|
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
|
|
|
|
val fileName = getPluginSanitizedFileName(internalName)
|
2022-08-07 21:11:13 +00:00
|
|
|
Log.i(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName")
|
2022-08-07 16:01:32 +00:00
|
|
|
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
|
2022-08-07 21:11:13 +00:00
|
|
|
val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName)
|
2022-08-06 23:43:39 +00:00
|
|
|
return loadPlugin(
|
2022-08-07 21:11:13 +00:00
|
|
|
activity,
|
2022-08-06 23:43:39 +00:00
|
|
|
file ?: return false,
|
2022-08-07 16:01:32 +00:00
|
|
|
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
2022-08-06 23:43:39 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-07 21:11:13 +00:00
|
|
|
suspend fun deletePlugin(pluginUrl: String): Boolean {
|
2022-08-06 23:43:39 +00:00
|
|
|
val data = getPluginsOnline()
|
|
|
|
.firstOrNull { it.url == pluginUrl }
|
|
|
|
?: return false
|
2022-08-07 21:11:13 +00:00
|
|
|
return try {
|
|
|
|
if (File(data.filePath).delete()) {
|
|
|
|
deletePluginData(data)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
false
|
|
|
|
} catch (e: Exception) {
|
|
|
|
false
|
|
|
|
}
|
2022-08-06 23:43:39 +00:00
|
|
|
}
|
2022-08-04 10:51:11 +00:00
|
|
|
}
|