diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 8a98bd2972e..90583011d19 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -408,13 +408,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa return true } - synchronized(apis) { - for (api in apis) { - if (str.startsWith(api.mainUrl)) { - loadResult(str, api.name, "") - return true - } - } + val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull() + if (matchedApi != null) { + loadResult(str, matchedApi.name, "") + return true } } } @@ -809,12 +806,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } - private val pluginsLock = Mutex() private fun onAllPluginsLoaded(success: Boolean = false) { ioSafe { pluginsLock.withLock { - synchronized(allProviders) { + allProviders.withLock { // Load cloned sites after plugins have been loaded since clones depend on plugins. try { getKey>(USER_PROVIDER_API)?.let { list -> @@ -1657,9 +1653,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ioSafe { initAll() // No duplicates (which can happen by registerMainAPI) - apis = synchronized(allProviders) { - allProviders.distinctBy { it } - } + apis = allProviders.distinctBy { it } } // val navView: BottomNavigationView = findViewById(R.id.nav_view) @@ -1967,7 +1961,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (BuildConfig.DEBUG) { var providersAndroidManifestString = "Current androidmanifest should be:\n" - synchronized(allProviders) { + allProviders.withLock { for (api in allProviders) { providersAndroidManifestString += " Unit)? = null -} \ No newline at end of file +} 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 eae14a6c0c3..6c8548241da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -689,22 +689,13 @@ object PluginManager { } // remove all registered apis - synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { - removePluginMapping(it) - } - } - synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { + removePluginMapping(it) } - synchronized(extractorApis) { - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } - } - - synchronized(VideoClickActionHolder.allVideoClickActions) { - VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } - } + APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename } + extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename } + VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename } synchronized(classLoaders) { classLoaders.values.removeIf { v -> v == plugin } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt index 83a7a09847c..9a1a441f144 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -36,7 +36,6 @@ import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.txt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt index 7a93f96f697..0b8c3e5ae8e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt @@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf /** Stateless safe abstraction of SubtitleAPI */ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @@ -24,26 +24,30 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { ) // maybe make this a generic struct? right now there is a lot of boilerplate - private val searchCache = threadSafeListOf() + private val searchCache = atomicListOf() private var searchCacheIndex: Int = 0 - private val resourceCache = threadSafeListOf() + private val resourceCache = atomicListOf() private var resourceCacheIndex: Int = 0 const val CACHE_SIZE = 20 } @WorkerThread suspend fun resource(data: SubtitleEntity): Result = runCatching { - synchronized(resourceCache) { + val cached = resourceCache.withLock { + var found: SubtitleResource? = null for (item in resourceCache) { // 20 min save if (item.query == data && (unixTime - item.unixTime) < 60 * 20) { - return@runCatching item.response + found = item.response + break } } + found } + if (cached != null) return@runCatching cached val returnValue = api.resource(freshAuth(), data) - synchronized(resourceCache) { + resourceCache.withLock { val add = SavedResourceResponse(unixTime, returnValue, data) if (resourceCache.size > CACHE_SIZE) { resourceCache[resourceCacheIndex] = add // rolling cache @@ -58,22 +62,25 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { @WorkerThread suspend fun search(query: SubtitleSearch): Result> { return runCatching { - synchronized(searchCache) { + val cached = searchCache.withLock { + var found: List? = null for (item in searchCache) { // 120 min save if (item.query == query && (unixTime - item.unixTime) < 60 * 120) { - return@runCatching item.response + found = item.response + break } } + found } - val returnValue = - api.search(freshAuth(), query) ?: emptyList() + if (cached != null) return@runCatching cached + val returnValue = api.search(freshAuth(), query) ?: emptyList() // only cache valid return values if (returnValue.isNotEmpty()) { val add = SavedSearchResponse(unixTime, returnValue, query) - synchronized(searchCache) { + searchCache.withLock { if (searchCache.size > CACHE_SIZE) { searchCache[searchCacheIndex] = add // rolling cache searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE @@ -86,4 +93,3 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) { } } } - diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 93a79689e50..8ec082520fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.newSearchResponseList -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) { val hash: Pair ) - private val cache = threadSafeListOf() + private val cache = atomicListOf() private var cacheIndex: Int = 0 const val CACHE_SIZE = 20 @@ -66,9 +66,7 @@ class APIRepository(val api: MainAPI) { private fun afterPluginsLoaded(forceReload: Boolean) { if (forceReload) { - synchronized(cache) { - cache.clear() - } + cache.clear() } } @@ -91,21 +89,25 @@ class APIRepository(val api: MainAPI) { val fixedUrl = api.fixUrl(url) val lookingForHash = Pair(api.name, fixedUrl) - synchronized(cache) { + val cached = cache.withLock { + var found: LoadResponse? = null for (item in cache) { // 10 min save if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { - return@withTimeout item.response + found = item.response + break } } + found } + if (cached != null) return@withTimeout cached api.load(fixedUrl)?.also { response -> // Remove all blank tags as early as possible response.tags = response.tags?.filter { it.isNotBlank() } val add = SavedLoadResponse(unixTime, response, lookingForHash) - synchronized(cache) { + cache.withLock { if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache cacheIndex = (cacheIndex + 1) % CACHE_SIZE @@ -215,4 +217,4 @@ class APIRepository(val api: MainAPI) { return false } } -} \ No newline at end of file +} 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 e0609c0e57b..8d48f5a6859 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 @@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) + return APIRepository(apis.withLock { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 6e28c128d1c..c5f8fa3d974 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -210,14 +210,13 @@ class LibraryFragment : BaseFragment( syncId: SyncIdName, apiName: String? = null, ) { - val availableProviders = synchronized(allProviders) { - allProviders.filter { - it.supportedSyncNames.contains(syncId) - }.map { it.name } + - // Add the api if it exists - (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } - ?: emptyList()) - } + val availableProviders = allProviders.filter { + it.supportedSyncNames.contains(syncId) + }.map { it.name } + + // Add the api if it exists + (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } + ?: emptyList()) + val baseOptions = listOf( LibraryOpenerType.Default, LibraryOpenerType.None, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 7dfe3cf5988..b48adbf4d48 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1685,14 +1685,13 @@ class ResultViewModel2 : ViewModel() { } val realRecommendations = ArrayList() - val apiNames = synchronized(apis) { - apis.filter { - it.name.contains("gogoanime", true) || - it.name.contains("9anime", true) - }.map { - it.name - } + val apiNames = apis.filter { + it.name.contains("gogoanime", true) || + it.name.contains("9anime", true) + }.map { + it.name } + meta.recommendations?.forEach { rec -> apiNames.forEach { name -> realRecommendations.add(rec.copy(apiName = name)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 27db8d1ae5e..f60588e35cd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() { private var suggestionJob: Job? = null - private var repos = synchronized(apis) { apis.map { APIRepository(it) } } + private var repos = apis.withLock { apis.map { APIRepository(it) } } fun clearSearch() { _searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false))) @@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun reloadRepos() { - repos = synchronized(apis) { apis.map { APIRepository(it) } } + repos = apis.withLock { apis.map { APIRepository(it) } } } fun searchAndCancel( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index dbf2ff1dc53..57f5aa87098 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { } fun showAdd() { - val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } + val providers = allProviders.distinctBy { it::class }.sortedBy { it.name } activity?.showDialog( providers.map { "${it.name} (${it.mainUrl})" }, -1, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 076f17a0aaf..c8478a84003 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() { getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener { activity?.getApiProviderLangSettings()?.let { currentLangTags -> - val languagesTagName = synchronized(APIHolder.apis) { - listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) + - APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } - .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji + val languagesTagName = APIHolder.apis.withLock { + listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) + + APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) } + .toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } } val currentIndexList = currentLangTags.map { langTag -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 818f1fd792f..22500d93199 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.MainAPI -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.TestingUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ class TestViewModel : ViewModel() { get() = scope != null private var filter = ProviderFilter.All - private val providers = threadSafeListOf>() + private val providers = atomicListOf>() private var passed = 0 private var failed = 0 private var total = 0 @@ -51,9 +51,9 @@ class TestViewModel : ViewModel() { } private fun postProviders() { - synchronized(providers) { + providers.withLock { val filtered = when (filter) { - ProviderFilter.All -> providers + ProviderFilter.All -> providers.toList() ProviderFilter.Passed -> providers.filter { it.second.success } ProviderFilter.Failed -> providers.filter { !it.second.success } } @@ -68,7 +68,7 @@ class TestViewModel : ViewModel() { } private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) { - synchronized(providers) { + providers.withLock { val index = providers.indexOfFirst { it.first == api } if (index == -1) { providers.add(api to results) @@ -81,14 +81,14 @@ class TestViewModel : ViewModel() { } fun init() { - total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size } + total = APIHolder.allProviders.withLock { APIHolder.allProviders.size } updateProgress() } fun startTest() { scope = CoroutineScope(Dispatchers.Default) - val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() } + val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() } total = apis.size failed = 0 passed = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 501ee0eef7b..8c2e8e34498 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment( if (isSetup) if ( // If any available languages - synchronized(apis) { apis.distinctBy { it.lang }.size > 1 } + apis.distinctBy { it.lang }.size > 1 ) { findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 3c4a09adea8..c18be8a2fdd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 1377ccd08ad..7278fcdd74f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -369,28 +369,10 @@ object AppContextUtils { } fun Context.getApiSettings(): HashSet { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() val activeLangs = getApiProviderLangSettings() val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } - .map { it.name }) - - /*val set = settingsManager.getStringSet( - this.getString(R.string.search_providers_list_key), - hashSet - )?.toHashSet() ?: hashSet - - val list = HashSet() - for (name in set) { - val api = getApiFromNameNull(name) ?: continue - if (activeLangs.contains(api.lang)) { - list.add(name) - } - }*/ - //if (list.isEmpty()) return hashSet - //return list + hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name }) return hashSet } @@ -481,9 +463,7 @@ object AppContextUtils { } ?: default val langs = this.getApiProviderLangSettings() val hasUniversal = langs.contains(AllLanguagesName) - val allApis = synchronized(apis) { - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } - } + val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } return if (currentPrefMedia.isEmpty()) { allApis } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 351e77c8d72..539d5e1a419 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -96,10 +96,8 @@ object SyncUtil { .mapNotNull { it.url }.toMutableList() if (type == "anilist") { // TODO MAKE BETTER - synchronized(apis) { - apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { - current.add("${it.mainUrl}/anime/$id") - } + apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { + current.add("${it.mainUrl}/anime/$id") } } return current diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a97145c3f81..5b5c52a7cef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ junitKtx = "1.3.0" junitVersion = "1.3.0" juniversalchardet = "2.5.0" kotlinGradlePlugin = "2.3.20" +kotlinxAtomicfu = "0.32.1" kotlinxCollectionsImmutable = "0.4.0" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" @@ -81,6 +82,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" } juniversalchardet = { module = "com.github.albfernandez:juniversalchardet", version.ref = "juniversalchardet" } +kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" } kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 073e49e6483..b5f525e8385 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -56,6 +56,7 @@ kotlin { implementation(libs.annotation) // Annotations implementation(libs.nicehttp) // HTTP Lib implementation(libs.jackson.module.kotlin) // JSON Parser + implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.coroutines.core) implementation(libs.fuzzywuzzy) // Match Extractors implementation(libs.jsoup) // HTML Parser diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 975572d054c..bc443b3f878 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -10,10 +10,10 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.requestCreator import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -119,7 +119,7 @@ actual class WebViewResolver actual constructor( } var fixedRequest: Request? = null - val extraRequestList = threadSafeListOf() + val extraRequestList = atomicListOf() main { try { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index c590165a1ad..77c8856acbc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import com.lagradost.cloudstream3.utils.Coroutines.mainWork -import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.nicehttp.RequestBodyTypes @@ -83,11 +83,10 @@ object APIHolder { val unixTimeMS: Long get() = System.currentTimeMillis() - // ConcurrentModificationException is possible!!! - val allProviders = threadSafeListOf() + val allProviders = atomicListOf() fun initAll() { - synchronized(allProviders) { + allProviders.withLock { for (api in allProviders) { api.init() } @@ -100,25 +99,25 @@ object APIHolder { return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } - var apis: List = threadSafeListOf() + var apis: AtomicList = atomicListOf() var apiMap: Map? = null fun addPluginMapping(plugin: MainAPI) { - synchronized(apis) { + apis.withLock { apis = apis + plugin } initMap(true) } fun removePluginMapping(plugin: MainAPI) { - synchronized(apis) { + apis.withLock { apis = apis.filter { it != plugin } } initMap(true) } private fun initMap(forcedUpdate: Boolean = false) { - synchronized(apis) { + apis.withLock { if (apiMap == null || forcedUpdate) apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() } @@ -126,24 +125,21 @@ object APIHolder { fun getApiFromNameNull(apiName: String?): MainAPI? { if (apiName == null) return null - synchronized(allProviders) { + return allProviders.withLock { initMap() - synchronized(apis) { - return apiMap?.get(apiName)?.let { apis.getOrNull(it) } + apis.withLock { + apiMap?.get(apiName)?.let { apis.getOrNull(it) } // Leave the ?. null check, it can crash regardless - ?: allProviders.firstOrNull { it.name == apiName } + ?: allProviders.firstOrNull { it.name == apiName } } } } fun getApiFromUrlNull(url: String?): MainAPI? { if (url == null) return null - synchronized(allProviders) { - allProviders.forEach { api -> - if (url.startsWith(api.mainUrl)) return api - } + return allProviders.withLock { + allProviders.firstOrNull { url.startsWith(it.mainUrl) } } - return null } /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 6fde6efe3b3..7076e407f59 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -30,11 +30,9 @@ class CrossTmdbProvider : TmdbProvider() { } private val validApis - get() = - synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } } + get() = apis.filter { it.lang == this.lang && it::class != this::class } //.distinctBy { it.uniqueId } - data class CrossMetaData( @JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("movies") val movies: List>? = null, @@ -121,4 +119,4 @@ class CrossTmdbProvider : TmdbProvider() { return base } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt index 61f87b8bab9..f4fce2ef33e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt @@ -17,10 +17,7 @@ abstract class BasePlugin { fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") element.sourcePlugin = this.filename - // Race condition causing which would case duplicates if not for distinctBy - synchronized(APIHolder.allProviders) { - APIHolder.allProviders.add(element) - } + APIHolder.allProviders.add(element) APIHolder.addPluginMapping(element) } @@ -31,9 +28,7 @@ abstract class BasePlugin { fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") element.sourcePlugin = this.filename - synchronized(extractorApis) { - extractorApis.add(element) - } + extractorApis.add(element) } /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt new file mode 100644 index 00000000000..975344271df --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AtomicList.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils + +import kotlinx.atomicfu.locks.SynchronizedObject +import kotlinx.atomicfu.locks.synchronized + +/** + * A thread-safe list backed by [SynchronizedObject]. + * + * For iteration, wrap block in [withLock] to hold the lock for the duration: + * list.withLock { list.forEach { ... } } + */ +open class AtomicList( + protected var delegate: List = emptyList() +) : List, SynchronizedObject() { + + fun withLock(block: () -> R): R = synchronized(this) { block() } + + fun set(newList: List) = synchronized(this) { delegate = newList } + fun filter(predicate: (T) -> Boolean): AtomicList = synchronized(this) { AtomicList(delegate.filter(predicate)) } + fun distinctBy(selector: (T) -> Any?): AtomicList = synchronized(this) { AtomicList(delegate.distinctBy(selector)) } + + override val size: Int get() = synchronized(this) { delegate.size } + override fun isEmpty(): Boolean = synchronized(this) { delegate.isEmpty() } + override fun contains(element: T): Boolean = synchronized(this) { delegate.contains(element) } + override fun containsAll(elements: Collection): Boolean = synchronized(this) { delegate.containsAll(elements) } + override fun get(index: Int): T = synchronized(this) { delegate[index] } + override fun indexOf(element: T): Int = synchronized(this) { delegate.indexOf(element) } + override fun lastIndexOf(element: T): Int = synchronized(this) { delegate.lastIndexOf(element) } + + // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. + override fun iterator(): Iterator = delegate.iterator() + override fun listIterator(): ListIterator = delegate.listIterator() + override fun listIterator(index: Int): ListIterator = delegate.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int): List = delegate.subList(fromIndex, toIndex) + + operator fun plus(element: T): AtomicList = synchronized(this) { AtomicList(delegate + element) } + operator fun plus(elements: Collection): AtomicList = synchronized(this) { AtomicList(delegate + elements) } +} + +class AtomicMutableList( + delegate: MutableList = mutableListOf() +) : AtomicList(delegate), MutableList { + + private val mutableDelegate get() = delegate as MutableList + + override fun add(element: T): Boolean = synchronized(this) { mutableDelegate.add(element) } + override fun add(index: Int, element: T) = synchronized(this) { mutableDelegate.add(index, element) } + override fun addAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(elements) } + override fun addAll(index: Int, elements: Collection): Boolean = synchronized(this) { mutableDelegate.addAll(index, elements) } + override fun remove(element: T): Boolean = synchronized(this) { mutableDelegate.remove(element) } + override fun removeAt(index: Int): T = synchronized(this) { mutableDelegate.removeAt(index) } + override fun removeAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.removeAll(elements) } + override fun retainAll(elements: Collection): Boolean = synchronized(this) { mutableDelegate.retainAll(elements) } + override fun set(index: Int, element: T): T = synchronized(this) { mutableDelegate.set(index, element) } + override fun clear() = synchronized(this) { mutableDelegate.clear() } + + // Iterators intentionally NOT synchronized, callers must use withLock { } for safe iteration. + override fun iterator(): MutableIterator = mutableDelegate.iterator() + override fun listIterator(): MutableListIterator = mutableDelegate.listIterator() + override fun listIterator(index: Int): MutableListIterator = mutableDelegate.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int): MutableList = mutableDelegate.subList(fromIndex, toIndex) +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index c525a1f36b0..d15ea129c29 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* -import java.util.Collections.synchronizedList @AnyThread expect fun runOnMainThreadNative(@MainThread work: (() -> Unit)) @@ -64,9 +64,18 @@ object Coroutines { /** * Safe to add and remove how you want * If you want to iterate over the list then you need to do: - * synchronized(allProviders) { code here } + * list.withLock { code here } */ - fun threadSafeListOf(vararg items: T): MutableList { - return synchronizedList(items.toMutableList()) + @Prerelease + fun atomicListOf(vararg items: T): AtomicMutableList { + return AtomicMutableList(items.toMutableList()) } + + // Deprecate after next stable + /*@Deprecated( + message = "Use atomicListOf() instead.", + replaceWith = ReplaceWith("atomicListOf(*items)"), + level = DeprecationLevel.WARNING, + )*/ + fun threadSafeListOf(vararg items: T): MutableList = atomicListOf(*items) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 45e2ba71e9f..b40dd90cf37 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -308,6 +308,7 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive @@ -904,7 +905,7 @@ suspend fun loadExtractor( return false } -val extractorApis: MutableList = arrayListOf( +val extractorApis: AtomicMutableList = atomicListOf( //AllProvider(), Mp4Upload(), StreamTape(),