forked from recloudstream/cloudstream
Add local file management
This commit is contained in:
parent
0dc8077296
commit
815838f17b
6 changed files with 156 additions and 68 deletions
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue