Revamped backend and added auto updating

This commit is contained in:
Blatzar 2022-08-07 18:01:32 +02:00
parent 8f3176d2cf
commit 89936c2bd6
9 changed files with 216 additions and 111 deletions

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.lagradost.cloudstream3"> package="com.lagradost.cloudstream3">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this --> <uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
@ -12,41 +12,41 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide--> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!--Used for app update--> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!--Used for app update-->
<!--<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run--> <!--<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run-->
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt;--> <!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt;-->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android--> <!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
<application <application
android:name=".AcraApplication" android:name=".AcraApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:appCategory="video"
android:banner="@mipmap/ic_banner" android:banner="@mipmap/ic_banner"
android:label="@string/app_name" android:fullBackupContent="@xml/backup_descriptor"
android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:label="@string/app_name"
android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme" android:supportsRtl="true"
android:fullBackupContent="@xml/backup_descriptor" android:theme="@style/AppTheme"
android:appCategory="video" android:usesCleartextTraffic="true"
tools:targetApi="o"> tools:targetApi="o">
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.lagradost.cloudstream3.utils.CastOptionsProvider" /> android:value="com.lagradost.cloudstream3.utils.CastOptionsProvider" />
<activity <activity
android:name=".ui.player.DownloadedPlayerActivity" android:name=".ui.player.DownloadedPlayerActivity"
android:screenOrientation="userLandscape" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true" android:screenOrientation="userLandscape"
android:exported="true"> android:supportsPictureInPicture="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -57,18 +57,18 @@
<!-- I dont think this label can be translated, but idk --> <!-- I dont think this label can be translated, but idk -->
<intent-filter android:label="@string/play_with_app_name"> <intent-filter android:label="@string/play_with_app_name">
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*"/> <data android:mimeType="*/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:exported="true" android:name=".MainActivity"
android:name=".MainActivity" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true">
<intent-filter android:exported="true"> <intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -83,35 +83,39 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="cloudstreamapp" /> <data android:scheme="cloudstreamapp" />
<data
android:host="cs.repo"
android:pathPrefix="/"
android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver <receiver
android:name=".receivers.VideoDownloadRestartReceiver" android:name=".receivers.VideoDownloadRestartReceiver"
android:enabled="false" android:enabled="false"
android:exported="true"> android:exported="true">
<intent-filter android:exported="true"> <intent-filter android:exported="true">
<action android:name="restart_service" /> <action android:name="restart_service" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:name=".services.VideoDownloadService" android:name=".services.VideoDownloadService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<activity <activity
android:exported="false" android:name=".ui.ControllerActivity"
android:name=".ui.ControllerActivity" /> android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
android:exported="false" android:enabled="true"
android:enabled="true" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>

View file

@ -420,6 +420,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
PluginManager.updateAllOnlinePlugins(applicationContext)
PluginManager.loadAllLocalPlugins(applicationContext) PluginManager.loadAllLocalPlugins(applicationContext)
PluginManager.loadAllOnlinePlugins(applicationContext) PluginManager.loadAllOnlinePlugins(applicationContext)

View file

@ -16,6 +16,7 @@ abstract class Plugin {
class Manifest { class Manifest {
var name: String? = null var name: String? = null
var pluginClassName: String? = null var pluginClassName: String? = null
var pluginVersion: Int? = null
} }
var resources: Resources? = null var resources: Resources? = null

View file

@ -17,6 +17,11 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.R 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.File import java.io.File
@ -27,13 +32,22 @@ import java.util.*
const val PLUGINS_KEY = "PLUGINS_KEY" const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL" const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
// Data class for internal storage
data class PluginData( data class PluginData(
@JsonProperty("name") val name: String, @JsonProperty("internalName") val internalName: String,
@JsonProperty("url") val url: String?, @JsonProperty("url") val url: String?,
@JsonProperty("isOnline") val isOnline: Boolean, @JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String, @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 { object PluginManager {
// Prevent multiple writes at once // Prevent multiple writes at once
val lock = Mutex() val lock = Mutex()
@ -80,22 +94,62 @@ object PluginManager {
private val LOCAL_PLUGINS_PATH = private val LOCAL_PLUGINS_PATH =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
// Maps filepath to plugin
private val plugins: MutableMap<String, Plugin> = private val plugins: MutableMap<String, Plugin> =
LinkedHashMap<String, Plugin>() LinkedHashMap<String, Plugin>()
private val classLoaders: MutableMap<PathClassLoader, Plugin> = private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>() HashMap<PathClassLoader, Plugin>()
private val failedToLoad: MutableMap<File, Any> = LinkedHashMap() private val failedToLoad: MutableMap<File, Any> = LinkedHashMap()
var loadedLocalPlugins = false private var loadedLocalPlugins = false
private val gson = Gson() private val gson = Gson()
private fun maybeLoadPlugin(context: Context, file: File) { private fun maybeLoadPlugin(context: Context, file: File) {
val name = file.name val name = file.name
if (file.extension == "zip" || file.extension == "cs3") { 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<Array<RepositoryData>>(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) { fun loadAllOnlinePlugins(context: Context) {
File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name } File(context.filesDir, ONLINE_PLUGINS_FOLDER).listFiles()?.sortedBy { it.name }
?.forEach { file -> ?.forEach { file ->
@ -130,12 +184,12 @@ object PluginManager {
* */ * */
private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean { private fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension val fileName = file.nameWithoutExtension
setPluginData(data) val filePath = file.absolutePath
println("Loading plugin: $data") println("Loading plugin: $data")
//logger.info("Loading plugin: " + fileName); //logger.info("Loading plugin: " + fileName);
return try { return try {
val loader = PathClassLoader(file.absolutePath, context.classLoader) val loader = PathClassLoader(filePath, context.classLoader)
var manifest: Plugin.Manifest var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream -> loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) { if (stream == null) {
@ -150,15 +204,21 @@ object PluginManager {
) )
} }
} }
val name: String = manifest.name ?: "NO NAME" val name: String = manifest.name ?: "NO NAME"
val version: Int = manifest.pluginVersion ?: PLUGIN_VERSION_NOT_SET
val pluginClass: Class<*> = val pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?> loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin = val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin pluginClass.newInstance() as Plugin
if (plugins.containsKey(name)) { if (plugins.containsKey(filePath)) {
println("Plugin with name $name already exists") println("Plugin with name $name already exists")
return false return true
} }
// Sets with the proper version
setPluginData(data.copy(version = version))
pluginInstance.__filename = fileName pluginInstance.__filename = fileName
if (pluginInstance.needsResources) { if (pluginInstance.needsResources) {
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
@ -172,9 +232,10 @@ object PluginManager {
context.resources.configuration context.resources.configuration
) )
} }
plugins[name] = pluginInstance plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
pluginInstance.load(context) pluginInstance.load(context)
println("Loaded plugin ${data.internalName} successfully")
true true
} catch (e: Throwable) { } catch (e: Throwable) {
failedToLoad[file] = e failedToLoad[file] = e
@ -188,12 +249,24 @@ object PluginManager {
} }
} }
suspend fun downloadPlugin(context: Context, pluginUrl: String, name: String): Boolean { suspend fun downloadAndLoadPlugin(
val file = downloadPluginToFile(context, pluginUrl, name) 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( return loadPlugin(
context, context,
file ?: return false, file ?: return false,
PluginData(name, pluginUrl, true, file.absolutePath) PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
) )
} }

View file

@ -3,16 +3,13 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 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.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.PROVIDER_STATUS_OK import com.lagradost.cloudstream3.apmapIndexed
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -36,17 +33,28 @@ data class Repository(
* 3: Beta only * 3: Beta only
* */ * */
data class SitePlugin( data class SitePlugin(
// Url to the .cs3 file
@JsonProperty("url") val url: String, @JsonProperty("url") val url: String,
// Status to remotely disable the provider
@JsonProperty("status") val status: Int, @JsonProperty("status") val status: Int,
// Integer over 0, any change of this will trigger an auto update
@JsonProperty("version") val version: Int, @JsonProperty("version") val version: Int,
// Unused currently, used to make the api backwards compatible?
// Set to 1
@JsonProperty("apiVersion") val apiVersion: Int, @JsonProperty("apiVersion") val apiVersion: Int,
// Name to be shown in app
@JsonProperty("name") val name: String, @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<String>, @JsonProperty("authors") val authors: List<String>,
@JsonProperty("description") val description: String?, @JsonProperty("description") val description: String?,
// Might be used to go directly to the plugin repo in the future
@JsonProperty("repositoryUrl") val repositoryUrl: String?, @JsonProperty("repositoryUrl") val repositoryUrl: String?,
// These types are yet to be mapped and used, ignore for now
@JsonProperty("tvTypes") val tvTypes: List<String>?, @JsonProperty("tvTypes") val tvTypes: List<String>?,
@JsonProperty("language") val language: String?, @JsonProperty("language") val language: String?,
@JsonProperty("iconUrl") val iconUrl: String?, @JsonProperty("iconUrl") val iconUrl: String?,
// Set to true to get an 18+ symbol next to the plugin
@JsonProperty("isAdult") val isAdult: Boolean?, @JsonProperty("isAdult") val isAdult: Boolean?,
) )
@ -69,21 +77,32 @@ object RepositoryManager {
return tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList() return tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
} }
suspend fun getRepoPlugins(repositoryUrl: String): List<SitePlugin>? { /**
* Gets all plugins from repositories and pairs them with the repository url
* */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
val repo = parseRepository(repositoryUrl) ?: return null val repo = parseRepository(repositoryUrl) ?: return null
return repo.pluginLists.apmap { return repo.pluginLists.apmapIndexed { index, url ->
parsePlugins(it) parsePlugins(url).map {
repo.pluginLists[index] to it
}
}.flatten() }.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 { return suspendSafeApiCall {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER) val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
if (!extensionsDir.exists()) if (!extensionsDir.exists())
extensionsDir.mkdirs() extensionsDir.mkdirs()
val newFile = File(extensionsDir, "$name.${pluginUrl.hashCode()}.cs3") val newDir = File(extensionsDir, folder)
if (newFile.exists()) return@suspendSafeApiCall newFile newDir.mkdirs()
val newFile = File(newDir, "${fileName}.cs3")
// Overwrite if exists
if (newFile.exists()) {
newFile.delete()
}
newFile.createNewFile() newFile.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body val body = app.get(pluginUrl).okhttpResponse.body
@ -95,21 +114,20 @@ object RepositoryManager {
// Don't want to read before we write in another thread // Don't want to read before we write in another thread
private val repoLock = Mutex() private val repoLock = Mutex()
suspend fun addRepository(repository: RepositoryData) { suspend fun addRepository(repository: RepositoryData) {
repoLock.withLock { repoLock.withLock {
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
// No duplicates // No duplicates
if (currentRepos.any { it.url == repository.url }) return setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
setKey(REPOSITORIES_KEY, currentRepos + repository) }
}
} }
suspend fun removeRepository(repository: RepositoryData) { suspend fun removeRepository(repository: RepositoryData) {
repoLock.withLock { repoLock.withLock {
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray() val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
// No duplicates // No duplicates
val newRepos = currentRepos.filter { it.url != repository.url } val newRepos = currentRepos.filter { it.url != repository.url }
setKey(REPOSITORIES_KEY, newRepos) setKey(REPOSITORIES_KEY, newRepos)
} }
} }
private fun write(stream: InputStream, output: OutputStream) { private fun write(stream: InputStream, output: OutputStream) {

View file

@ -25,7 +25,7 @@ class ExtensionsViewModel : ViewModel() {
_repositories.postValue(urls) _repositories.postValue(urls)
} }
suspend fun getPlugins(repositoryUrl: String): List<SitePlugin> { suspend fun getPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>> {
return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList() return RepositoryManager.getRepoPlugins(repositoryUrl) ?: emptyList()
} }
} }

View file

@ -12,8 +12,8 @@ import com.lagradost.cloudstream3.plugins.SitePlugin
import kotlinx.android.synthetic.main.repository_item.view.* import kotlinx.android.synthetic.main.repository_item.view.*
class PluginAdapter( class PluginAdapter(
var plugins: List<SitePlugin>, var plugins: List<Pair<String, SitePlugin>>,
val iconClickCallback: PluginAdapter.(plugin: SitePlugin, isDownloaded: Boolean) -> Unit val iconClickCallback: PluginAdapter.(repositoryUrl: String, plugin: SitePlugin, isDownloaded: Boolean) -> Unit
) : ) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -23,9 +23,10 @@ class PluginAdapter(
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val (repositoryUrl, plugin) = plugins[position]
when (holder) { when (holder) {
is PluginViewHolder -> { is PluginViewHolder -> {
holder.bind(plugins[position]) holder.bind(repositoryUrl, plugin)
} }
} }
} }
@ -44,7 +45,8 @@ class PluginAdapter(
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
fun bind( fun bind(
plugin: SitePlugin repositoryUrl: String,
plugin: SitePlugin,
) { ) {
val isDownloaded = storedPlugins.any { it.url == plugin.url } val isDownloaded = storedPlugins.any { it.url == plugin.url }
@ -56,7 +58,7 @@ class PluginAdapter(
itemView.action_button?.setImageResource(drawableInt) itemView.action_button?.setImageResource(drawableInt)
itemView.action_button?.setOnClickListener { itemView.action_button?.setOnClickListener {
iconClickCallback.invoke(this@PluginAdapter, plugin, isDownloaded) iconClickCallback.invoke(this@PluginAdapter, repositoryUrl, plugin, isDownloaded)
} }
itemView.main_text?.text = plugin.name itemView.main_text?.text = plugin.name

View file

@ -10,7 +10,6 @@ import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.PluginManager 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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -44,29 +43,35 @@ class PluginsFragment : Fragment() {
ioSafe { ioSafe {
val plugins = extensionViewModel.getPlugins(url) val plugins = extensionViewModel.getPlugins(url)
main { main {
repo_recycler_view?.adapter = PluginAdapter(plugins) { plugin, isDownloaded -> repo_recycler_view?.adapter =
ioSafe { PluginAdapter(plugins) { repositoryUrl, plugin, isDownloaded ->
val (success, message) = if (isDownloaded) { ioSafe {
PluginManager.deletePlugin(view.context, plugin.url, plugin.name) to R.string.plugin_deleted val (success, message) = if (isDownloaded) {
} else { PluginManager.deletePlugin(
PluginManager.downloadPlugin( view.context,
view.context, plugin.url,
plugin.url, plugin.name
plugin.name ) to R.string.plugin_deleted
) to R.string.plugin_loaded } else {
} PluginManager.downloadAndLoadPlugin(
view.context,
plugin.url,
plugin.name,
repositoryUrl
) to R.string.plugin_loaded
}
println("Success: $success") println("Success: $success")
if (success) { if (success) {
main { main {
showToast(activity, message, Toast.LENGTH_SHORT) showToast(activity, message, Toast.LENGTH_SHORT)
this@PluginAdapter.reloadStoredPlugins() this@PluginAdapter.reloadStoredPlugins()
// Dirty and needs a fix // Dirty and needs a fix
repo_recycler_view?.adapter?.notifyDataSetChanged() repo_recycler_view?.adapter?.notifyDataSetChanged()
}
} }
} }
} }
}
} }
} }
} }

View file

@ -426,11 +426,12 @@ object VideoDownloadManager {
} }
private const val reservedChars = "|\\?*<\":>+[]/\'" private const val reservedChars = "|\\?*<\":>+[]/\'"
fun sanitizeFilename(name: String): String { fun sanitizeFilename(name: String, removeSpaces: Boolean= false): String {
var tempName = name var tempName = name
for (c in reservedChars) { for (c in reservedChars) {
tempName = tempName.replace(c, ' ') tempName = tempName.replace(c, ' ')
} }
if (removeSpaces) tempName = tempName.replace(" ", "")
return tempName.replace(" ", " ").trim(' ') return tempName.replace(" ", " ").trim(' ')
} }