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.app.Activity
import android.util.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
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.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.CommonActivity.showToast 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.plugins.RepositoryManager.getRepoPlugins
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.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -45,7 +41,24 @@ data class PluginData(
@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, @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 // This is used as a placeholder / not set version
const val PLUGIN_VERSION_NOT_SET = Int.MIN_VALUE 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 { lock.withLock {
if (data.isOnline) { if (data.isOnline) {
val plugins = getPluginsOnline().filter { it.url != data.url } val plugins = getPluginsOnline().filter { it.url != data.url }
setKey(PLUGINS_KEY, plugins) setKey(PLUGINS_KEY, plugins)
} else { } else {
val plugins = getPluginsLocal().filter { it.filePath != data.filePath } 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> { fun getPluginsOnline(): Array<PluginData> {
return getKey(PLUGINS_KEY) ?: emptyArray() return getKey(PLUGINS_KEY) ?: emptyArray()
} }
@ -346,10 +369,14 @@ object PluginManager {
) )
} }
suspend fun deletePlugin(pluginUrl: String): Boolean { /**
val data = getPluginsOnline() * @param isFilePath will treat the pluginUrl as as the filepath instead of url
.firstOrNull { it.url == pluginUrl } * */
?: return false 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 { return try {
if (File(data.filePath).delete()) { if (File(data.filePath).delete()) {
unloadPlugin(data.filePath) unloadPlugin(data.filePath)

View file

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

View file

@ -59,10 +59,12 @@ class ExtensionsFragment : Fragment() {
repo_recycler_view?.adapter = RepoAdapter(false, { repo_recycler_view?.adapter = RepoAdapter(false, {
findNavController().navigate( findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins, R.id.navigation_settings_extensions_to_navigation_settings_plugins,
Bundle().apply { PluginsFragment.newInstance(
putString(PLUGINS_BUNDLE_NAME, it.name) it.name,
putString(PLUGINS_BUNDLE_URL, it.url) it.url,
}) false
)
)
}, { repo -> }, { repo ->
// Prompt user before deleting repo // Prompt user before deleting repo
main { 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 { add_repo_button?.setOnClickListener {
val builder = val builder =
AlertDialog.Builder(context ?: return@setOnClickListener, R.style.AlertDialogCustom) 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_NAME = "name"
const val PLUGINS_BUNDLE_URL = "url" const val PLUGINS_BUNDLE_URL = "url"
const val PLUGINS_BUNDLE_LOCAL = "isLocal"
class PluginsFragment : Fragment() { class PluginsFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
@ -29,6 +30,7 @@ class PluginsFragment : Fragment() {
val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val name = arguments?.getString(PLUGINS_BUNDLE_NAME)
val url = arguments?.getString(PLUGINS_BUNDLE_URL) val url = arguments?.getString(PLUGINS_BUNDLE_URL)
val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true
if (url == null || name == null) { if (url == null || name == null) {
activity?.onBackPressed() activity?.onBackPressed()
@ -49,21 +51,28 @@ class PluginsFragment : Fragment() {
plugin_recycler_view?.adapter = plugin_recycler_view?.adapter =
PluginAdapter { PluginAdapter {
pluginViewModel.handlePluginAction(activity, url, it) pluginViewModel.handlePluginAction(activity, url, it, isLocal)
} }
observe(pluginViewModel.plugins) { observe(pluginViewModel.plugins) {
(plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(it) (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 { companion object {
fun newInstance(name: String, url: String): Bundle { fun newInstance(name: String, url: String, isLocal: Boolean): Bundle {
return Bundle().apply { return Bundle().apply {
putString(PLUGINS_BUNDLE_NAME, name) putString(PLUGINS_BUNDLE_NAME, name)
putString(PLUGINS_BUNDLE_URL, url) 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 * @param viewModel optional, updates the plugins livedata for that viewModel if included
* */ * */
fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe { fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) =
if (activity == null) return@ioSafe ioSafe {
val stored = getDownloads() if (activity == null) return@ioSafe
val plugins = getPlugins(repositoryUrl) val stored = getDownloads()
val plugins = getPlugins(repositoryUrl)
plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list -> plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list ->
main { main {
showToast( 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, activity,
if (list.isEmpty()) { metadata.url,
metadata.name,
repo
)
}.main { list ->
if (list.any { it }) {
showToast(
activity,
txt( txt(
R.string.batch_download_nothing_to_download_format, R.string.batch_download_finish_format,
txt(R.string.plugin) list.count { it },
)
} else {
txt(
R.string.batch_download_start_format,
list.size,
txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin) txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin)
) ),
}, Toast.LENGTH_SHORT
Toast.LENGTH_SHORT )
) viewModel?.updatePluginListPrivate(repositoryUrl)
} } else if (list.isNotEmpty()) {
}.apmap { (repo, metadata) -> showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
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)
} }
} }
}
} }
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") Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin")
if (activity == null) return@ioSafe if (activity == null) return@ioSafe
val (repo, metadata) = plugin val (repo, metadata) = plugin
val (success, message) = if (isDownloaded(plugin)) { val (success, message) = if (isDownloaded(plugin) || isLocal) {
PluginManager.deletePlugin( PluginManager.deletePlugin(
metadata.url, metadata.url,
isLocal
) to R.string.plugin_deleted ) to R.string.plugin_deleted
} else { } else {
PluginManager.downloadAndLoadPlugin( PluginManager.downloadAndLoadPlugin(
@ -136,7 +147,10 @@ class PluginsViewModel : ViewModel() {
} }
if (success) if (success)
updatePluginListPrivate(repositoryUrl) if (isLocal)
updatePluginListLocal()
else
updatePluginListPrivate(repositoryUrl)
} }
private suspend fun updatePluginListPrivate(repositoryUrl: String) { private suspend fun updatePluginListPrivate(repositoryUrl: String) {
@ -153,4 +167,20 @@ class PluginsViewModel : ViewModel() {
Log.i(TAG, "updatePluginList = $repositoryUrl") Log.i(TAG, "updatePluginList = $repositoryUrl")
updatePluginListPrivate(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:name="url"
android:defaultValue="@null" android:defaultValue="@null"
app:argType="string" /> app:argType="string" />
<argument
android:name="isLocal"
android:defaultValue="false"
app:argType="boolean" />
</action> </action>
</fragment> </fragment>