cloudstream/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt

404 lines
14 KiB
Kotlin
Raw Normal View History

2022-08-06 15:48:00 +00:00
package com.lagradost.cloudstream3.plugins
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
2022-08-11 22:36:19 +00:00
import com.lagradost.cloudstream3.*
2022-08-06 23:43:39 +00:00
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
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
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
2022-08-18 00:54:05 +00:00
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
2022-08-09 08:33:16 +00:00
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.extractorApis
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"
// Data class for internal storage
2022-08-06 23:43:39 +00:00
data class PluginData(
@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,
@JsonProperty("version") val version: Int,
2022-08-11 22:36:19 +00:00
) {
fun toSitePlugin(): SitePlugin {
return SitePlugin(
this.filePath,
PROVIDER_STATUS_OK,
maxOf(1, version),
1,
internalName,
internalName,
emptyList(),
File(this.filePath).name,
null,
null,
null,
null,
File(this.filePath).length()
2022-08-11 22:36:19 +00:00
)
}
}
2022-08-06 23:43:39 +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()
2022-08-17 21:59:21 +00:00
val newPlugins = plugins.filter { it.filePath != data.filePath } + data
setKey(PLUGINS_KEY, newPlugins)
2022-08-07 21:11:13 +00:00
} else {
val plugins = getPluginsLocal()
2022-08-17 21:59:21 +00:00
setKey(PLUGINS_KEY_LOCAL, plugins.filter { it.filePath != data.filePath } + data)
2022-08-06 23:43:39 +00:00
}
}
}
2022-08-11 22:36:19 +00:00
private suspend fun deletePluginData(data: PluginData?) {
if (data == null) return
2022-08-07 21:11:13 +00:00
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 }
2022-08-11 22:36:19 +00:00
setKey(PLUGINS_KEY_LOCAL, plugins)
2022-08-06 23:43:39 +00:00
}
}
}
2022-08-11 22:36:19 +00:00
suspend fun deleteRepositoryData(repositoryPath: String) {
lock.withLock {
val plugins = getPluginsOnline().filter {
!it.filePath.contains(repositoryPath)
}
setKey(PLUGINS_KEY, plugins)
}
}
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
// Maps filepath to plugin
2022-08-06 15:48:00 +00:00
private val plugins: MutableMap<String, Plugin> =
LinkedHashMap<String, Plugin>()
// Maps urls to plugin
val urlPlugins: MutableMap<String, Plugin> =
LinkedHashMap<String, Plugin>()
2022-08-06 15:48:00 +00:00
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>()
2022-08-11 17:39:34 +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") {
loadPlugin(
2022-08-07 21:11:13 +00:00
activity,
file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
)
2022-08-09 08:33:16 +00:00
} else {
Log.i(TAG, "Skipping invalid plugin file: $file")
}
}
2022-08-08 18:52:03 +00:00
// Helper class for updateAllOnlinePluginsAndLoadThem
2022-08-11 17:39:34 +00:00
data class OnlinePluginData(
2022-08-08 18:52:03 +00:00
val savedData: PluginData,
val onlineData: Pair<String, SitePlugin>,
) {
val isOutdated =
2022-08-17 21:59:21 +00:00
onlineData.second.version != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
2022-08-08 18:52:03 +00:00
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
}
2022-08-11 17:39:34 +00:00
var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
/**
* 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-08 18:52:03 +00:00
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
2022-08-11 17:39:34 +00:00
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
2022-08-08 18:52:03 +00:00
}.flatten().distinctBy { it.second.url }
// 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-08 18:52:03 +00:00
}.flatten().distinctBy { it.onlineData.second.url }
2022-08-11 17:39:34 +00:00
allCurrentOutDatedPlugins = outdatedPlugins.toSet()
2022-08-08 18:52:03 +00:00
Log.i(TAG, "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}")
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 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) {
2022-08-08 20:02:02 +00:00
Log.w(TAG, "Failed to create local directories")
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
Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins")
2022-08-09 08:33:16 +00:00
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
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
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-08 20:02:02 +00:00
Log.e(TAG, "Failed to load plugin $fileName: No manifest found")
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-17 21:59:21 +00:00
val name: String = manifest.name ?: "NO NAME".also {
Log.d(TAG, "No manifest name for ${data.internalName}")
}
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
Log.d(TAG, "No manifest version for ${data.internalName}")
}
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
// 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-09 16:14:36 +00:00
if (manifest.requiresResources) {
Log.d(TAG, "Loading resources for ${data.internalName}")
2022-08-04 10:51:11 +00:00
// 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
}
plugins[filePath] = pluginInstance
2022-08-06 15:48:00 +00:00
classLoaders[loader] = pluginInstance
if (data.url != null) { // TODO: make this cleaner
urlPlugins[data.url] = 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) {
2022-08-08 20:02:02 +00:00
Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}")
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
private fun unloadPlugin(absolutePath: String) {
2022-08-09 12:17:29 +00:00
Log.i(TAG, "Unloading plugin: $absolutePath")
val plugin = plugins[absolutePath]
2022-08-09 08:33:16 +00:00
if (plugin == null) {
Log.w(TAG, "Couldn't find plugin $absolutePath")
return
}
try {
plugin.beforeUnload()
} catch (e: Throwable) {
Log.e(TAG, "Failed to run beforeUnload $absolutePath: ${Log.getStackTraceString(e)}")
}
// remove all registered apis
2022-08-18 00:54:05 +00:00
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
removePluginMapping(it)
}
2022-08-09 15:19:26 +00:00
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
2022-08-09 08:33:16 +00:00
2022-08-11 17:39:34 +00:00
classLoaders.values.removeIf { v -> v == plugin }
2022-08-09 08:33:16 +00:00
plugins.remove(absolutePath)
}
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()
}
suspend fun downloadAndLoadPlugin(
2022-08-07 21:11:13 +00:00
activity: Activity,
pluginUrl: String,
internalName: String,
repositoryUrl: String
): Boolean {
2022-08-18 00:54:05 +00:00
try {
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
val fileName = getPluginSanitizedFileName(internalName)
Log.i(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName)
return loadPlugin(
activity,
file ?: return false,
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
)
} catch (e : Exception) {
logError(e)
return false
}
2022-08-06 23:43:39 +00:00
}
2022-08-11 22:36:19 +00:00
/**
* @param isFilePath will treat the pluginUrl as as the filepath instead of url
* */
suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean {
val data =
(if (isFilePath) (getPluginsLocal() + getPluginsOnline()).firstOrNull { it.filePath == pluginIdentifier }
2022-08-11 22:36:19 +00:00
else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false
2022-08-07 21:11:13 +00:00
return try {
if (File(data.filePath).delete()) {
2022-08-09 08:33:16 +00:00
unloadPlugin(data.filePath)
2022-08-07 21:11:13 +00:00
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
}