diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 49864e65..629406aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -84,6 +84,8 @@ import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_result_swipe.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.io.File import java.net.URI import kotlin.reflect.KClass @@ -131,6 +133,10 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" + + /** + * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread + * */ val afterPluginsLoadedEvent = Event() val mainPluginsLoadedEvent = Event() // homepage api, used to speed up time to load for homepage @@ -236,6 +242,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() + afterPluginsLoadedEvent += ::onAllPluginsLoaded try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession @@ -325,6 +332,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { broadcastIntent.action = "restart_service" broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java) this.sendBroadcast(broadcastIntent) + afterPluginsLoadedEvent -= ::onAllPluginsLoaded super.onDestroy() } @@ -414,6 +422,36 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } + private val pluginsLock = Mutex() + private fun onAllPluginsLoaded(success: Boolean = false) { + ioSafe { + pluginsLock.withLock { + // Load cloned sites after plugins have been loaded since clones depend on plugins. + try { + getKey>(USER_PROVIDER_API)?.let { list -> + list.forEach { custom -> + allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } + ?.let { + allProviders.add(it.javaClass.newInstance().apply { + name = custom.name + lang = custom.lang + mainUrl = custom.url.trimEnd('/') + canBeOverridden = false + }) + } + } + } + // it.hashCode() is not enough to make sure they are distinct + apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } + APIHolder.apiMap = null + } catch (e: Exception) { + logError(e) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -446,36 +484,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { mainPluginsLoadedEvent.invoke(false) } - if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { - PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) - } else { - PluginManager.loadAllOnlinePlugins(this@MainActivity) - } - - PluginManager.loadAllLocalPlugins(this@MainActivity) - - // Load cloned sites after plugins have been loaded since clones depend on plugins. - try { - getKey>(USER_PROVIDER_API)?.let { list -> - list.forEach { custom -> - allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } - ?.let { - allProviders.add(it.javaClass.newInstance().apply { - name = custom.name - lang = custom.lang - mainUrl = custom.url.trimEnd('/') - canBeOverridden = false - }) - } - } + ioSafe { + if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { + PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) + } else { + PluginManager.loadAllOnlinePlugins(this@MainActivity) } - apis = allProviders.distinctBy { it } - APIHolder.apiMap = null - } catch (e: Exception) { - logError(e) } - afterPluginsLoadedEvent.invoke(true) + ioSafe { + PluginManager.loadAllLocalPlugins(this@MainActivity) + } } // ioSafe { diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index ceacb9aa..7dc8dba7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -9,6 +9,7 @@ import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking import okhttp3.* +import java.net.URI @AnyThread @@ -30,6 +31,17 @@ class CloudflareKiller : Interceptor { val savedCookies: MutableMap> = mutableMapOf() + /** + * Gets the headers with cookies, webview user agent included! + * */ + fun getCookieHeaders(url: String): Headers { + val userAgentHeaders = WebViewResolver.webViewUserAgent?.let { + mapOf("user-agent" to it) + } ?: emptyMap() + + return getHeaders(userAgentHeaders, savedCookies[URI(url).host] ?: emptyMap()) + } + override fun intercept(chain: Interceptor.Chain): Response = runBlocking { val request = chain.request() val cookies = savedCookies[request.url.host] diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 96a2222e..59dae640 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -21,12 +21,15 @@ 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.removePluginMapping +import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.extractorApis import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.acra.log.debug import java.io.File import java.io.InputStreamReader import java.util.* @@ -162,11 +165,16 @@ object PluginManager { val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN } - var allCurrentOutDatedPlugins: Set = emptySet() + // var allCurrentOutDatedPlugins: Set = emptySet() suspend fun loadSinglePlugin(activity: Activity, apiName: String): Boolean { - return (getPluginsOnline().firstOrNull { it.internalName == apiName } - ?: getPluginsLocal().firstOrNull { it.internalName == apiName })?.let { savedData -> + return (getPluginsOnline().firstOrNull { + // Most of the time the provider ends with Provider which isn't part of the api name + it.internalName.replace("provider", "", ignoreCase = true) == apiName + } + ?: getPluginsLocal().firstOrNull { + it.internalName.replace("provider", "", ignoreCase = true) == apiName + })?.let { savedData -> // OnlinePluginData(savedData, onlineData) loadPlugin( activity, @@ -184,6 +192,19 @@ object PluginManager { * 4. Else load the plugin normally **/ fun updateAllOnlinePluginsAndLoadThem(activity: Activity) { + // Load all plugins as fast as possible! + (getPluginsOnline()).toList().apmap { pluginData -> + loadPlugin( + activity, + File(pluginData.filePath), + pluginData + ) + } + + ioSafe { + afterPluginsLoadedEvent.invoke(true) + } + val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES @@ -198,29 +219,28 @@ object PluginManager { OnlinePluginData(savedData, onlineData) } }.flatten().distinctBy { it.onlineData.second.url } - allCurrentOutDatedPlugins = outdatedPlugins.toSet() - Log.i(TAG, "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}") + debug { + "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" + } - outdatedPlugins.apmap { - if (it.isDisabled) { - return@apmap - } else if (it.isOutdated) { + outdatedPlugins.apmap { pluginData -> + if (pluginData.isDisabled) { + unloadPlugin(pluginData.savedData.filePath) + } else if (pluginData.isOutdated) { downloadAndLoadPlugin( activity, - it.onlineData.second.url, - it.savedData.internalName, - it.onlineData.first - ) - } else { - loadPlugin( - activity, - File(it.savedData.filePath), - it.savedData + pluginData.onlineData.second.url, + pluginData.savedData.internalName, + pluginData.onlineData.first ) } } + ioSafe { + afterPluginsLoadedEvent.invoke(true) + } + Log.i(TAG, "Plugin update done!") } @@ -256,6 +276,7 @@ object PluginManager { } loadedLocalPlugins = true + afterPluginsLoadedEvent.invoke(true) } /** @@ -380,7 +401,9 @@ object PluginManager { try { val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique val fileName = getPluginSanitizedFileName(internalName) - Log.i(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName") + unloadPlugin("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") + + Log.d(TAG, "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(activity, pluginUrl, fileName, folderName) return loadPlugin( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt index 73160db4..bbce21e9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/GithubApi.kt @@ -148,7 +148,6 @@ class GithubApi(index: Int) : InAppAuthAPIManager(index){ } } catch (e: Exception) { logError(e) - switchToOldAccount() } switchToOldAccount() return false @@ -179,4 +178,4 @@ class GithubApi(index: Int) : InAppAuthAPIManager(index){ ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 975545ee..539ce843 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -456,20 +456,17 @@ class HomeFragment : Fragment() { homeViewModel.loadStoredData(list) } - private var hasBeenConsumed = false private fun firstLoadHomePage(successful: Boolean = false) { // dirty hack to make it only load once - if(hasBeenConsumed) return - hasBeenConsumed = true - loadHomePage(successful) + loadHomePage(false) } - private fun loadHomePage(successful: Boolean = false) { + private fun loadHomePage(forceReload: Boolean = true) { val apiName = context?.getKey(USER_SELECTED_HOMEPAGE_API) if (homeViewModel.apiName.value != apiName || apiName == null) { //println("Caught home: " + homeViewModel.apiName.value + " at " + apiName) - homeViewModel.loadAndCancel(apiName) + homeViewModel.loadAndCancel(apiName, forceReload) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 7319c5b2..1d6ed584 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -264,25 +264,33 @@ class HomeViewModel : ViewModel() { } } - fun loadAndCancel(preferredApiName: String?) = viewModelScope.launchSafe { - val api = getApiFromNameNull(preferredApiName) - if (preferredApiName == noneApi.name){ - setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) - loadAndCancel(noneApi) - } - else if (preferredApiName == randomApi.name || api == null) { - val validAPIs = context?.filterProviderByPreferredMedia() - if (validAPIs.isNullOrEmpty()) { - // Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded - loadAndCancel(noneApi) - } else { - val apiRandom = validAPIs.random() - loadAndCancel(apiRandom) - setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = + viewModelScope.launchSafe { + // Since plugins are loaded in stages this function can get called multiple times. + // The issue with this is that the homepage may be fetched multiple times while the first request is loading + val api = getApiFromNameNull(preferredApiName) + if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { + return@launchSafe + } + // If the plugin isn't loaded yet. (Does not set the key) + if (api == null) { + loadAndCancel(noneApi) + } else if (preferredApiName == noneApi.name) { + setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + loadAndCancel(noneApi) + } else if (preferredApiName == randomApi.name) { + val validAPIs = context?.filterProviderByPreferredMedia() + if (validAPIs.isNullOrEmpty()) { + // Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded + loadAndCancel(noneApi) + } else { + val apiRandom = validAPIs.random() + loadAndCancel(apiRandom) + setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + } + } else { + setKey(USER_SELECTED_HOMEPAGE_API, api.name) + loadAndCancel(api) } - } else { - setKey(USER_SELECTED_HOMEPAGE_API, api.name) - loadAndCancel(api) } - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 0eb72f19..5f6ef921 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -6,6 +6,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.widget.FrameLayout +import androidx.preference.PreferenceManager import com.google.android.exoplayer2.* import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource @@ -742,7 +743,15 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - handleEvent(CSPlayerEvent.NextEpisode) + // Only play next episode if autoplay is on (default) + if (PreferenceManager.getDefaultSharedPreferences(context) + ?.getBoolean( + context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key), + true + ) == true + ) { + handleEvent(CSPlayerEvent.NextEpisode) + } } Player.STATE_BUFFERING -> { updatedTime() @@ -780,7 +789,15 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - handleEvent(CSPlayerEvent.NextEpisode) + // Only play next episode if autoplay is on (default) + if (PreferenceManager.getDefaultSharedPreferences(context) + ?.getBoolean( + context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key), + true + ) == true + ) { + handleEvent(CSPlayerEvent.NextEpisode) + } } Player.STATE_BUFFERING -> { updatedTime() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd367ac4..72bc55b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ double_tap_pause_enabled_key double_tap_seek_time_key swipe_vertical_enabled_key + autoplay_next_key display_sub_key show_fillers_key show_trailers_key @@ -42,7 +43,7 @@ primary_color_key restore_key backup_key - prefer_media_type_key + prefer_media_type_key_2 app_theme_key episode_sync_enabled_key log_enabled_key @@ -223,6 +224,10 @@ Swipe left or right to control time in the videoplayer Swipe to change settings Swipe on the left or right side to change brightness or volume + + Autoplay next episode + Start the next episode when the current one ends + Double tap to seek Double tap to pause Player seek amount diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index d98f506f..a7e36488 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -58,6 +58,12 @@ android:title="@string/swipe_to_change_settings" android:summary="@string/swipe_to_change_settings_des" app:defaultValue="true" /> +