Add local file management

This commit is contained in:
Blatzar 2022-08-12 00:36:19 +02:00
parent 0dc8077296
commit 815838f17b
6 changed files with 156 additions and 68 deletions

View File

@ -9,22 +9,18 @@ import android.widget.Toast
import android.app.Activity
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
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
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
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 com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.extractorApis
import kotlinx.coroutines.sync.Mutex
@ -45,7 +41,24 @@ data class PluginData(
@JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String,
@JsonProperty("version") val version: Int,
)
) {
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
)
}
}
// This is used as a placeholder / not set version
const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE
@ -74,18 +87,28 @@ object PluginManager {
}
}
private suspend fun deletePluginData(data: PluginData) {
private suspend fun deletePluginData(data: PluginData?) {
if (data == null) return
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)
setKey(PLUGINS_KEY_LOCAL, plugins)
}
}
}
suspend fun deleteRepositoryData(repositoryPath: String) {
lock.withLock {
val plugins = getPluginsOnline().filter {
!it.filePath.contains(repositoryPath)
}
setKey(PLUGINS_KEY, plugins)
}
}
fun getPluginsOnline(): Array<PluginData> {
return getKey(PLUGINS_KEY) ?: emptyArray()
}
@ -346,10 +369,14 @@ object PluginManager {
)
}
suspend fun deletePlugin(pluginUrl: String): Boolean {
val data = getPluginsOnline()
.firstOrNull { it.url == pluginUrl }
?: return false
/**
* @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().firstOrNull { it.filePath == pluginIdentifier }
else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false
return try {
if (File(data.filePath).delete()) {
unloadPlugin(data.filePath)

View File

@ -4,10 +4,12 @@ import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
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.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsLocal
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -93,9 +95,9 @@ object RepositoryManager {
* */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
val repo = parseRepository(repositoryUrl) ?: return null
return repo.pluginLists.apmapIndexed { index, url ->
return repo.pluginLists.apmap { url ->
parsePlugins(url).map {
repo.pluginLists[index] to it
repositoryUrl to it
}
}.flatten()
}
@ -150,10 +152,13 @@ object RepositoryManager {
setKey(REPOSITORIES_KEY, newRepos)
}
File(
val file = File(
extensionsDir,
getPluginSanitizedFileName(repository.url)
).delete()
)
PluginManager.deleteRepositoryData(file.absolutePath)
file.delete()
}
private fun write(stream: InputStream, output: OutputStream) {

View File

@ -59,10 +59,12 @@ class ExtensionsFragment : Fragment() {
repo_recycler_view?.adapter = RepoAdapter(false, {
findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
Bundle().apply {
putString(PLUGINS_BUNDLE_NAME, it.name)
putString(PLUGINS_BUNDLE_URL, it.url)
})
PluginsFragment.newInstance(
it.name,
it.url,
false
)
)
}, { repo ->
// Prompt user before deleting repo
main {
@ -120,6 +122,17 @@ class ExtensionsFragment : Fragment() {
}
}
plugin_storage_appbar?.setOnClickListener {
findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
PluginsFragment.newInstance(
getString(R.string.extensions),
"",
true
)
)
}
add_repo_button?.setOnClickListener {
val builder =
AlertDialog.Builder(context ?: return@setOnClickListener, R.style.AlertDialogCustom)

View File

@ -12,6 +12,7 @@ import kotlinx.android.synthetic.main.fragment_plugins.*
const val PLUGINS_BUNDLE_NAME = "name"
const val PLUGINS_BUNDLE_URL = "url"
const val PLUGINS_BUNDLE_LOCAL = "isLocal"
class PluginsFragment : Fragment() {
override fun onCreateView(
@ -29,6 +30,7 @@ class PluginsFragment : Fragment() {
val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
val url = arguments?.getString(PLUGINS_BUNDLE_URL)
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
if (url == null || name == null) {
activity?.onBackPressed()
@ -49,21 +51,28 @@ class PluginsFragment : Fragment() {
plugin_recycler_view?.adapter =
PluginAdapter {
pluginViewModel.handlePluginAction(activity, url, it)
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
}
observe(pluginViewModel.plugins) {
(plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(it)
}
pluginViewModel.updatePluginList(url)
if (isLocal) {
// No download button
settings_toolbar?.menu?.clear()
pluginViewModel.updatePluginListLocal()
} else {
pluginViewModel.updatePluginList(url)
}
}
companion object {
fun newInstance(name: String, url: String): Bundle {
fun newInstance(name: String, url: String, isLocal: Boolean): Bundle {
return Bundle().apply {
putString(PLUGINS_BUNDLE_NAME, name)
putString(PLUGINS_BUNDLE_URL, url)
putBoolean(PLUGINS_BUNDLE_LOCAL, isLocal)
}
}
}

View File

@ -59,65 +59,76 @@ class PluginsViewModel : ViewModel() {
/**
* @param viewModel optional, updates the plugins livedata for that viewModel if included
* */
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe {
if (activity == null) return@ioSafe
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl)
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
ioSafe {
if (activity == null) return@ioSafe
val stored = getDownloads()
val plugins = getPlugins(repositoryUrl)
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list ->
main {
showToast(
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list ->
main {
showToast(
activity,
if (list.isEmpty()) {
txt(
R.string.batch_download_nothing_to_download_format,
txt(R.string.plugin)
)
} else {
txt(
R.string.batch_download_start_format,
list.size,
txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin)
)
},
Toast.LENGTH_SHORT
)
}
}.apmap { (repo, metadata) ->
PluginManager.downloadAndLoadPlugin(
activity,
if (list.isEmpty()) {
metadata.url,
metadata.name,
repo
)
}.main { list ->
if (list.any { it }) {
showToast(
activity,
txt(
R.string.batch_download_nothing_to_download_format,
txt(R.string.plugin)
)
} else {
txt(
R.string.batch_download_start_format,
list.size,
R.string.batch_download_finish_format,
list.count { it },
txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin)
)
},
Toast.LENGTH_SHORT
)
}
}.apmap { (repo, metadata) ->
PluginManager.downloadAndLoadPlugin(
activity,
metadata.url,
metadata.name,
repo
)
}.main { list ->
if (list.any { it }) {
showToast(
activity,
txt(
R.string.batch_download_finish_format,
list.count { it },
txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin)
),
Toast.LENGTH_SHORT
)
viewModel?.updatePluginListPrivate(repositoryUrl)
} else if (list.isNotEmpty()) {
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
),
Toast.LENGTH_SHORT
)
viewModel?.updatePluginListPrivate(repositoryUrl)
} else if (list.isNotEmpty()) {
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
}
}
}
}
}
fun handlePluginAction(activity: Activity?, repositoryUrl: String, plugin: Plugin) = ioSafe {
/**
* @param isLocal defines if the plugin data is from local data instead of repo
* Will only allow removal of plugins. Used for the local file management.
* */
fun handlePluginAction(
activity: Activity?,
repositoryUrl: String,
plugin: Plugin,
isLocal: Boolean
) = ioSafe {
Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin")
if (activity == null) return@ioSafe
val (repo, metadata) = plugin
val (success, message) = if (isDownloaded(plugin)) {
val (success, message) = if (isDownloaded(plugin) || isLocal) {
PluginManager.deletePlugin(
metadata.url,
isLocal
) to R.string.plugin_deleted
} else {
PluginManager.downloadAndLoadPlugin(
@ -136,7 +147,10 @@ class PluginsViewModel : ViewModel() {
}
if (success)
updatePluginListPrivate(repositoryUrl)
if (isLocal)
updatePluginListLocal()
else
updatePluginListPrivate(repositoryUrl)
}
private suspend fun updatePluginListPrivate(repositoryUrl: String) {
@ -153,4 +167,20 @@ class PluginsViewModel : ViewModel() {
Log.i(TAG, "updatePluginList = $repositoryUrl")
updatePluginListPrivate(repositoryUrl)
}
/**
* Update the list but only with the local data. Used for file management.
* */
fun updatePluginListLocal() = viewModelScope.launch {
Log.i(TAG, "updatePluginList = local")
val downloadedPlugins = (PluginManager.getPluginsOnline() + PluginManager.getPluginsLocal())
.distinctBy { it.filePath }
.map {
PluginViewData("" to it.toSitePlugin(), true)
}
_plugins.postValue(downloadedPlugins)
}
}

View File

@ -177,6 +177,10 @@
android:name="url"
android:defaultValue="@null"
app:argType="string" />
<argument
android:name="isLocal"
android:defaultValue="false"
app:argType="boolean" />
</action>
</fragment>