Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
+ [Contributing:](#contributing)
+ [Issues:](#issues)
+ [Bugs Reports:](#bug_report)
+ [Enhancement:](#enhancment)
+ [Enhancement:](#enhancement)
+ [Extension Development:](#extensions)
+ [Language Support:](#languages)
+ [Further Sources](#contact_and_sources)


<a id="about_us"></a>
<a id="about_us" name="about_us"></a>

## About us:

Expand Down Expand Up @@ -49,45 +49,46 @@ Our documentation is unmaintained and open to contributions; therefore, apps and
+ Extension system for personal customization


<a id="install_rules"></a>
<a id="install_rules" name="install_rules"></a>

## Installation:

Our documentation provides the steps to install and configure CloudStream for your streaming needs.

[Getting Started With CloudStream:](https://recloudstream.github.io/csdocs/)

<a id="contributing"></a>
<a id="contributing" name="contributing"></a>

## Contributing:
We **happily** accept any contributions to our project. To find out where you can start contributing towards the project, please look [at our issues tab](/cloudstream/issues)
We **happily** accept any contributions to our project. To find out where you can start contributing towards the project, please look [at our issues tab](https://github.com/recloudstream/cloudstream/issues)



<a id="issues"></a>
<a id="issues" name="issues"></a>

### Issues:
While we **actively** accept issues and pull requests, we do require you fill out an [template](https://github.com/recloudstream/cloudstream/issues/new/choose) for issues. These include the following:

<a id="bug_report"></a>
<a id="bug_report" name="bug_report"></a>

- [Bug Report Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=bug&projects=&template=application-bug.yml)
- For bug reports, we want as much info as possible, including your downloaded version of CloudeStream, device and updated version (if possible, current API),
expected behavior of the program, and the actual behavior that the program did, most importantly we require clear, reproducible steps of the bug. If your bug can't be reproduced, it is unlikely we'll work on your issue.

<a id="enhancment"></a>
<a id="enhancement" name="enhancement"></a>

- [Feature Request Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=enhancement&projects=&template=feature-request.yml)
- Before adding a feature request, please check to see if a feature request already has been requested.


### Extensions:
<a id="extensions" name="extensions"></a>

**Further details on creating extensions for CloudStream are found in our documentation.**

[Guide: For Extension Developers](https://recloudstream.github.io/csdocs/devs/gettingstarted/)

<a id="contact_and_sources"></a>
<a id="contact_and_sources" name="contact_and_sources"></a>

## Further Sources:

Expand All @@ -100,7 +101,7 @@ As well as providing clear install steps, our [website](https://dweb.link/ipns/c
- [Linux](https://recloudstream.github.io/csdocs/other-devices/linux/)
- And more...

<a id="languages"> </a>
<a id="languages" name="languages"></a>

### Supported languages:

Expand Down
9 changes: 6 additions & 3 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager.___DO_NOT_CALL_FROM_A_PL
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.services.MaintenanceWorkManager
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
Expand Down Expand Up @@ -637,7 +638,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

override fun onResume() {
super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded
setActivityInstance(this)
try {
if (isCastApiAvailable()) {
Expand Down Expand Up @@ -722,7 +722,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
broadcastIntent.action = "restart_service"
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
this.sendBroadcast(broadcastIntent)
afterPluginsLoadedEvent -= ::onAllPluginsLoaded
detachBackPressedCallback("MainActivityDefault")
super.onDestroy()
}
Expand Down Expand Up @@ -1197,6 +1196,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
setNavigationBarColorCompat(R.attr.primaryGrayBackground)
updateLocale()
super.onCreate(savedInstanceState)
afterPluginsLoadedEvent += ::onAllPluginsLoaded
MaintenanceWorkManager.enqueuePeriodicWork(this)
try {
if (isCastApiAvailable()) {
CastContext.getSharedInstance(this) { it.run() }
Expand All @@ -1222,7 +1223,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
safe {
// Recompile oat on new version
PluginManager.deleteAllOatFiles(this)
ioSafe {
PluginManager.deleteAllOatFiles(this@MainActivity)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.InternalAPI
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.PROVIDER_STATUS_OK
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
Expand Down Expand Up @@ -67,6 +66,7 @@ import java.io.InputStreamReader
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
const val PLUGINS_KEY = "PLUGINS_KEY"
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
const val PLUGINS_DISABLED_KEY = "PLUGINS_DISABLED_KEY"

const val EXTENSIONS_CHANNEL_ID = "cloudstream3.extensions"
const val EXTENSIONS_CHANNEL_NAME = "Extensions"
Expand Down Expand Up @@ -163,7 +163,7 @@ object PluginManager {
* This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update.
*/
fun deleteAllOatFiles(context: Context) {
File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo ->
File("${context.filesDir}/$ONLINE_PLUGINS_FOLDER").listFiles()?.forEach { repo ->
repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file ->
val success = file.deleteRecursively()
Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success")
Expand All @@ -180,6 +180,23 @@ object PluginManager {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
}

fun getDisabledPlugins(): Set<String> {
return getKey<Array<String>>(PLUGINS_DISABLED_KEY)?.toSet() ?: emptySet()
}

fun setPluginDisabled(path: String, disabled: Boolean) {
val current = getDisabledPlugins().toMutableSet()
if (disabled) {
current.add(path)
unloadPlugin(path)
} else {
current.remove(path)
// Note: Enabling requires reloading which might need context.
// For now, we just update the persistent list.
}
setKey(PLUGINS_DISABLED_KEY, current.toTypedArray())
}

private val CLOUD_STREAM_FOLDER =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/"

Expand All @@ -189,11 +206,11 @@ object PluginManager {

// Maps filepath to plugin
val plugins: MutableMap<String, BasePlugin> =
LinkedHashMap<String, BasePlugin>()
LinkedHashMap()

// Maps urls to plugin
val urlPlugins: MutableMap<String, BasePlugin> =
LinkedHashMap<String, BasePlugin>()
LinkedHashMap()

private val classLoaders: MutableMap<PathClassLoader, BasePlugin> =
HashMap<PathClassLoader, BasePlugin>()
Expand All @@ -208,9 +225,15 @@ object PluginManager {
val name = file.name
if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin(
context,
file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
context = context,
file = file,
data = PluginData(
internalName = name,
url = null,
isOnline = false,
filePath = file.absolutePath,
version = PLUGIN_VERSION_NOT_SET
)
)
} else {
Log.i(TAG, "Skipping invalid plugin file: $file")
Expand Down Expand Up @@ -255,6 +278,17 @@ object PluginManager {
} ?: false
}

suspend fun loadSinglePluginByPath(context: Context, path: String): Boolean {
return (getPluginsOnline().firstOrNull { it.filePath == path }
?: getPluginsLocal().firstOrNull { it.filePath == path })?.let { savedData ->
loadPlugin(
context,
File(savedData.filePath),
savedData
)
} ?: false
}

/**
* Needs to be run before other plugin loading because plugin loading can not be overwritten
* 1. Gets all online data about the downloaded plugins
Expand Down Expand Up @@ -460,13 +494,16 @@ object PluginManager {
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context: Context) {
assertNonRecursiveCallstack()

val disabled = getDisabledPlugins()
// Load all plugins as fast as possible!
(getPluginsOnline()).toList().amap { pluginData ->
loadPlugin(
context,
File(pluginData.filePath),
pluginData
)
if (pluginData.filePath !in disabled) {
loadPlugin(
context,
File(pluginData.filePath),
pluginData
)
}
}
}

Expand Down Expand Up @@ -529,9 +566,11 @@ object PluginManager {
// Make sure all local plugins are fully refreshed.
removeKey(PLUGINS_KEY_LOCAL)

val disabled = getDisabledPlugins()
sortedPlugins?.sortedBy { it.name }?.amap { file ->
try {
val destinationFile = File(pluginDirectory, file.name)
if (destinationFile.absolutePath in disabled) return@amap

// Only copy the file if the destination file doesn't exist or if it
// has been modified (check file length and modification time).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.lagradost.cloudstream3.services

import android.content.Context
import androidx.work.*
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.TimeUnit

class MaintenanceWorkManager(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
companion object {
const val MAINTENANCE_WORK_NAME = "work_maintenance"

fun enqueuePeriodicWork(context: Context?) {
if (context == null) return

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(requiresBatteryNotLow = true)
.build()

val periodicWork =
PeriodicWorkRequest.Builder(MaintenanceWorkManager::class.java, 24, TimeUnit.HOURS)
.addTag(MAINTENANCE_WORK_NAME)
.setConstraints(constraints)
.build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
MAINTENANCE_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
periodicWork
)
}
}

override suspend fun doWork(): Result {
try {
// Load all plugins
PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context)
PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(
context = context,
forceReload = false
)

val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() }

// Map to track successes per plugin path
val pluginSuccess = mutableMapOf<String, Boolean>()
val pluginPaths = apis.mapNotNull { it.sourcePlugin }.distinct()
pluginPaths.forEach { pluginSuccess[it] = false }

val scope = CoroutineScope(Dispatchers.Default + kotlinx.coroutines.Job())
TestingUtils.getDeferredProviderTests(scope, apis) { api, result ->
val path = api.sourcePlugin ?: return@getDeferredProviderTests
if (result.success) {
pluginSuccess[path] = true
}
}

// Wait for tests to finish
scope.coroutineContext[kotlinx.coroutines.Job]?.children?.forEach { it.join() }

pluginSuccess.forEach { (path, success) ->
if (!success) {
// All providers in this plugin failed
PluginManager.setPluginDisabled(path = path, disabled = true)
}
}

return Result.success()
} catch (ignored: Throwable) {
return Result.failure()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,11 @@ class GeneratorPlayer : FullScreenPlayer() {
}
if (init) {
sortedUrls.getOrNull(sourceIndex)?.let {
it.first?.source?.let { source ->
viewModel.state.generatorState?.id?.let { parentId ->
DataStoreHelper.setSourcePreference(parentId, source)
}
}
loadLink(it, true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.videoskip.SkipAPI
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
import kotlinx.collections.immutable.PersistentList
Expand Down Expand Up @@ -66,9 +67,16 @@ data class VideoState(
* Use .links if order is not needed */
@Contract(pure = true)
fun sortLinks(qualityProfile: Int): List<VideoLink> {
return sortedLinks[qualityProfile] ?: links.sortedBy { link ->
val preferredSource = generatorState?.id?.let { DataStoreHelper.getSourcePreference(it) }
return sortedLinks[qualityProfile] ?: links.sortedWith { a, b ->
if (preferredSource != null) {
val aPreferred = a.first?.source == preferredSource
val bPreferred = b.first?.source == preferredSource
if (aPreferred && !bPreferred) return@sortedWith -1
if (!aPreferred && bPreferred) return@sortedWith 1
}
// negative because we want to sort highest quality first
-getLinkPriority(qualityProfile, link.first)
compareValuesBy(b, a) { getLinkPriority(qualityProfile, it.first) }
}.also { value -> sortedLinks[qualityProfile] = value }
}

Expand Down
Loading