From 89936c2bd673b1509edaeffa1b9504919e3c24ec Mon Sep 17 00:00:00 2001 From: Blatzar <46196380+Blatzar@users.noreply.github.com> Date: Sun, 7 Aug 2022 18:01:32 +0200 Subject: [PATCH] Revamped backend and added auto updating --- app/src/main/AndroidManifest.xml | 104 +++++++++--------- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../lagradost/cloudstream3/plugins/Plugin.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 97 ++++++++++++++-- .../cloudstream3/plugins/RepositoryManager.kt | 62 +++++++---- .../extensions/ExtensionsViewModel.kt | 2 +- .../ui/settings/extensions/PluginAdapter.kt | 12 +- .../ui/settings/extensions/PluginsFragment.kt | 45 ++++---- .../utils/VideoDownloadManager.kt | 3 +- 9 files changed, 216 insertions(+), 111 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 14ab2c8d..fefa5b57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.lagradost.cloudstream3"> @@ -12,41 +12,41 @@ - + + android:name="android.hardware.touchscreen" + android:required="false" /> + android:name="android.software.leanback" + android:required="false" /> + android:name=".AcraApplication" + android:allowBackup="true" + android:appCategory="video" + android:banner="@mipmap/ic_banner" + android:fullBackupContent="@xml/backup_descriptor" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true" + tools:targetApi="o"> + android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" + android:value="com.lagradost.cloudstream3.utils.CastOptionsProvider" /> + android:name=".ui.player.DownloadedPlayerActivity" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" + android:exported="true" + android:resizeableActivity="true" + android:screenOrientation="userLandscape" + android:supportsPictureInPicture="true"> @@ -57,18 +57,18 @@ - - - + + + + android:name=".MainActivity" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" + android:exported="true" + android:resizeableActivity="true" + android:supportsPictureInPicture="true"> @@ -83,35 +83,39 @@ + + android:name=".receivers.VideoDownloadRestartReceiver" + android:enabled="false" + android:exported="true"> + android:name=".services.VideoDownloadService" + android:enabled="true" + android:exported="false" /> + android:name=".ui.ControllerActivity" + android:exported="false" /> + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.provider" + android:enabled="true" + android:exported="false" + android:grantUriPermissions="true"> + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths" /> \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 0bbdd358..6efe94ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -420,6 +420,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) + PluginManager.updateAllOnlinePlugins(applicationContext) PluginManager.loadAllLocalPlugins(applicationContext) PluginManager.loadAllOnlinePlugins(applicationContext) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index a2f29a55..5e20469c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -16,6 +16,7 @@ abstract class Plugin { class Manifest { var name: String? = null var pluginClassName: String? = null + var pluginVersion: Int? = null } var resources: Resources? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index d7f42e4f..2acc0eec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -17,6 +17,11 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.R +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 import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -27,13 +32,22 @@ import java.util.* const val PLUGINS_KEY = "PLUGINS_KEY" const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL" + +// Data class for internal storage data class PluginData( - @JsonProperty("name") val name: String, + @JsonProperty("internalName") val internalName: String, @JsonProperty("url") val url: String?, @JsonProperty("isOnline") val isOnline: Boolean, @JsonProperty("filePath") val filePath: String, + @JsonProperty("version") val version: Int, ) +// 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 + object PluginManager { // Prevent multiple writes at once val lock = Mutex() @@ -80,22 +94,62 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" - + // Maps filepath to plugin private val plugins: MutableMap = LinkedHashMap() + private val classLoaders: MutableMap = HashMap() + private val failedToLoad: MutableMap = LinkedHashMap() - var loadedLocalPlugins = false + private var loadedLocalPlugins = false private val gson = Gson() private fun maybeLoadPlugin(context: Context, file: File) { val name = file.name if (file.extension == "zip" || file.extension == "cs3") { - loadPlugin(context, file, PluginData(name, null, false, file.absolutePath)) + loadPlugin( + context, + file, + PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET) + ) } } + /** + * Needs to be run before other plugin loading because plugin loading can not be overwritten + **/ + fun updateAllOnlinePlugins(context: Context) { + val urls = getKey>(REPOSITORIES_KEY) ?: emptyArray() + + val onlinePlugins = urls.toList().apmap { + getRepoPlugins(it.url)?.toList() ?: emptyList() + }.flatten() + + // 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 } + .mapNotNull { onlineData -> + val isOutdated = + onlineData.second.apiVersion != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE + if (isOutdated) savedData to onlineData else null + } + }.flatten() + + println("Outdated plugins: $outdatedPlugins") + + outdatedPlugins.apmap { + downloadAndLoadPlugin( + context, + it.second.second.url, + it.first.internalName, + it.second.first + ) + } + + println("Plugin update done!") + } + fun loadAllOnlinePlugins(context: Context) { File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name } ?.forEach { file -> @@ -130,12 +184,12 @@ object PluginManager { * */ private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { val fileName = file.nameWithoutExtension - setPluginData(data) + val filePath = file.absolutePath println("Loading plugin: $data") //logger.info("Loading plugin: " + fileName); return try { - val loader = PathClassLoader(file.absolutePath, context.classLoader) + val loader = PathClassLoader(filePath, context.classLoader) var manifest: Plugin.Manifest loader.getResourceAsStream("manifest.json").use { stream -> if (stream == null) { @@ -150,15 +204,21 @@ object PluginManager { ) } } + val name: String = manifest.name ?: "NO NAME" + val version: Int = manifest.pluginVersion ?: PLUGIN_VERSION_NOT_SET val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = pluginClass.newInstance() as Plugin - if (plugins.containsKey(name)) { + if (plugins.containsKey(filePath)) { println("Plugin with name $name already exists") - return false + return true } + + // Sets with the proper version + setPluginData(data.copy(version = version)) + pluginInstance.__filename = fileName if (pluginInstance.needsResources) { // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk @@ -172,9 +232,10 @@ object PluginManager { context.resources.configuration ) } - plugins[name] = pluginInstance + plugins[filePath] = pluginInstance classLoaders[loader] = pluginInstance pluginInstance.load(context) + println("Loaded plugin ${data.internalName} successfully") true } catch (e: Throwable) { failedToLoad[file] = e @@ -188,12 +249,24 @@ object PluginManager { } } - suspend fun downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean { - val file = downloadPluginToFile(context, pluginUrl, name) + suspend fun downloadAndLoadPlugin( + context: Context, + pluginUrl: String, + internalName: String, + repositoryUrl: String + ): Boolean { + val folderName = (sanitizeFilename( + repositoryUrl, + true + ) + "." + repositoryUrl.hashCode()) // Guaranteed unique + val fileName = (sanitizeFilename(internalName, true) + "." + internalName.hashCode()) + println("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(context, pluginUrl, fileName, folderName) return loadPlugin( context, file ?: return false, - PluginData(name, pluginUrl, true, file.absolutePath) + PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index a3fb7857..92fb9fe0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -3,16 +3,13 @@ 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.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.PROVIDER_STATUS_OK -import com.lagradost.cloudstream3.apmap +import com.lagradost.cloudstream3.apmapIndexed import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall 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 com.lagradost.cloudstream3.utils.Coroutines.ioSafe import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.BufferedInputStream @@ -36,17 +33,28 @@ data class Repository( * 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("isAdult") val isAdult: Boolean?, ) @@ -69,21 +77,32 @@ object RepositoryManager { return tryParseJson>(response.text)?.toList() ?: emptyList() } - suspend fun getRepoPlugins(repositoryUrl: String): List? { + /** + * 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.apmap { - parsePlugins(it) + return repo.pluginLists.apmapIndexed { index, url -> + parsePlugins(url).map { + repo.pluginLists[index] to it + } }.flatten() } - suspend fun downloadPluginToFile(context: Context, pluginUrl: String, name: String): File? { + 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 newFile = File(extensionsDir, "$name.${pluginUrl.hashCode()}.cs3") - if (newFile.exists()) return@suspendSafeApiCall newFile + 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 @@ -95,21 +114,20 @@ object RepositoryManager { // 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 - if (currentRepos.any { it.url == repository.url }) return - setKey(REPOSITORIES_KEY, currentRepos + repository) - } + repoLock.withLock { + val currentRepos = getKey>(REPOSITORIES_KEY) ?: emptyArray() + // No duplicates + setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url }) + } } suspend fun removeRepository(repository: RepositoryData) { - repoLock.withLock { - val currentRepos = getKey>(REPOSITORIES_KEY) ?: emptyArray() - // No duplicates - val newRepos = currentRepos.filter { it.url != repository.url } - setKey(REPOSITORIES_KEY, newRepos) - } + repoLock.withLock { + val currentRepos = getKey>(REPOSITORIES_KEY) ?: emptyArray() + // No duplicates + val newRepos = currentRepos.filter { it.url != repository.url } + setKey(REPOSITORIES_KEY, newRepos) + } } private fun write(stream: InputStream, output: OutputStream) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index 842a65b2..d25dc4ad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -25,7 +25,7 @@ class ExtensionsViewModel : ViewModel() { _repositories.postValue(urls) } - suspend fun getPlugins(repositoryUrl: String): List { + suspend fun getPlugins(repositoryUrl: String): List> { return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index af42d228..a8746852 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -12,8 +12,8 @@ import com.lagradost.cloudstream3.plugins.SitePlugin import kotlinx.android.synthetic.main.repository_item.view.* class PluginAdapter( - var plugins: List, - val iconClickCallback: PluginAdapter.(plugin: SitePlugin, isDownloaded: Boolean) -> Unit + var plugins: List>, + val iconClickCallback: PluginAdapter.(repositoryUrl: String, plugin: SitePlugin, isDownloaded: Boolean) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -23,9 +23,10 @@ class PluginAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val (repositoryUrl, plugin) = plugins[position] when (holder) { is PluginViewHolder -> { - holder.bind(plugins[position]) + holder.bind(repositoryUrl, plugin) } } } @@ -44,7 +45,8 @@ class PluginAdapter( RecyclerView.ViewHolder(itemView) { fun bind( - plugin: SitePlugin + repositoryUrl: String, + plugin: SitePlugin, ) { val isDownloaded = storedPlugins.any { it.url == plugin.url } @@ -56,7 +58,7 @@ class PluginAdapter( itemView.action_button?.setImageResource(drawableInt) itemView.action_button?.setOnClickListener { - iconClickCallback.invoke(this@PluginAdapter, plugin, isDownloaded) + iconClickCallback.invoke(this@PluginAdapter, repositoryUrl, plugin, isDownloaded) } itemView.main_text?.text = plugin.name diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index 2367b515..0b8d9247 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -10,7 +10,6 @@ import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -44,29 +43,35 @@ class PluginsFragment : Fragment() { ioSafe { val plugins = extensionViewModel.getPlugins(url) main { - repo_recycler_view?.adapter = PluginAdapter(plugins) { plugin, isDownloaded -> - ioSafe { - val (success, message) = if (isDownloaded) { - PluginManager.deletePlugin(view.context, plugin.url, plugin.name) to R.string.plugin_deleted - } else { - PluginManager.downloadPlugin( - view.context, - plugin.url, - plugin.name - ) to R.string.plugin_loaded - } + repo_recycler_view?.adapter = + PluginAdapter(plugins) { repositoryUrl, plugin, isDownloaded -> + ioSafe { + val (success, message) = if (isDownloaded) { + PluginManager.deletePlugin( + view.context, + plugin.url, + plugin.name + ) to R.string.plugin_deleted + } else { + PluginManager.downloadAndLoadPlugin( + view.context, + plugin.url, + plugin.name, + repositoryUrl + ) to R.string.plugin_loaded + } - println("Success: $success") - if (success) { - main { - showToast(activity, message, Toast.LENGTH_SHORT) - this@PluginAdapter.reloadStoredPlugins() - // Dirty and needs a fix - repo_recycler_view?.adapter?.notifyDataSetChanged() + println("Success: $success") + if (success) { + main { + showToast(activity, message, Toast.LENGTH_SHORT) + this@PluginAdapter.reloadStoredPlugins() + // Dirty and needs a fix + repo_recycler_view?.adapter?.notifyDataSetChanged() + } } } } - } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index c4d05cfe..f74762a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -426,11 +426,12 @@ object VideoDownloadManager { } private const val reservedChars = "|\\?*<\":>+[]/\'" - fun sanitizeFilename(name: String): String { + fun sanitizeFilename(name: String, removeSpaces: Boolean= false): String { var tempName = name for (c in reservedChars) { tempName = tempName.replace(c, ' ') } + if (removeSpaces) tempName = tempName.replace(" ", "") return tempName.replace(" ", " ").trim(' ') }