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(' ')
}