Skip to content
Open
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
20 changes: 18 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.android)
// alias(libs.plugins.dokka)

// alias(libs.plugins.google.services) // We use manual Firebase initialization
}

val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
Expand Down Expand Up @@ -217,6 +218,19 @@ dependencies {
// Downloading & Networking
implementation(libs.work.runtime.ktx)
implementation(libs.nicehttp) // HTTP Lib

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.firestore)
implementation(libs.firebase.auth)
implementation(libs.firebase.analytics)

configurations.all {
resolutionStrategy {
force("com.google.protobuf:protobuf-javalite:3.25.1")
}
exclude(group = "com.google.protobuf", module = "protobuf-java")
}

implementation(project(":library") {
// There does not seem to be a good way of getting the android flavor.
Expand Down Expand Up @@ -269,6 +283,7 @@ tasks.withType<KotlinJvmCompile> {
}
}

/*
dokka {
moduleName = "App"
dokkaSourceSets {
Expand All @@ -287,3 +302,4 @@ dokka {
}
}
}
*/
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.lagradost.cloudstream3

import android.content.Context
import com.lagradost.api.setContext
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.getKey
import com.lagradost.cloudstream3.utils.removeKeys
import com.lagradost.cloudstream3.utils.setKey
import java.lang.ref.WeakReference

/**
Expand Down
11 changes: 6 additions & 5 deletions app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStore
import com.lagradost.cloudstream3.utils.getKey
import com.lagradost.cloudstream3.utils.getKeys
import com.lagradost.cloudstream3.utils.removeKey
import com.lagradost.cloudstream3.utils.removeKeys
import com.lagradost.cloudstream3.utils.setKey
import com.lagradost.cloudstream3.utils.ImageLoader.buildImageLoader
import kotlinx.coroutines.runBlocking
import java.io.File
Expand Down
27 changes: 24 additions & 3 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
Expand All @@ -40,6 +42,7 @@ import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
Expand Down Expand Up @@ -138,6 +141,7 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache
import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.FirestoreSyncManager
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers
import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
Expand All @@ -152,8 +156,8 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.getKey
import com.lagradost.cloudstream3.utils.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
Expand Down Expand Up @@ -261,6 +265,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
*/
val reloadAccountEvent = Event<Boolean>()

/**
* Used to notify HomeViewModel that sync data (specifically Continue Watching) has been updated
*/
val syncUpdatedEvent = Event<Boolean>()

/**
* @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
Expand Down Expand Up @@ -620,6 +629,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

override fun onResume() {
super.onResume()
if (FirestoreSyncManager.isEnabled(this)) {
FirestoreSyncManager.pushAllLocalData(this)
}
afterPluginsLoadedEvent += ::onAllPluginsLoaded
setActivityInstance(this)
try {
Expand All @@ -633,7 +645,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa

override fun onPause() {
super.onPause()

if (FirestoreSyncManager.isEnabled(this)) {
FirestoreSyncManager.pushAllLocalData(this)
}
// Start any delayed updates
if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
Expand Down Expand Up @@ -1191,6 +1205,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} catch (t: Throwable) {
logError(t)
}

lifecycleScope.launch(Dispatchers.IO) {
FirestoreSyncManager.initialize(this@MainActivity)
}

window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv()
Expand Down Expand Up @@ -1653,6 +1671,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val navController = navHostFragment.navController

navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
if (FirestoreSyncManager.isEnabled(this@MainActivity)) {
FirestoreSyncManager.syncNow(this@MainActivity)
}
// Intercept search and add a query
updateNavBar(navDestination)
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.FirestoreSyncManager
import com.lagradost.cloudstream3.utils.txt
import dalvik.system.PathClassLoader
import kotlinx.coroutines.sync.Mutex
Expand All @@ -75,6 +76,8 @@ data class PluginData(
@JsonProperty("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String,
@JsonProperty("version") val version: Int,
@JsonProperty("addedDate") val addedDate: Long = 0,
@JsonProperty("isDeleted") val isDeleted: Boolean = false,
) {
fun toSitePlugin(): SitePlugin {
return SitePlugin(
Expand Down Expand Up @@ -109,14 +112,29 @@ object PluginManager {

private var hasCreatedNotChanel = false

/**
* Store data about the plugin for fetching later
* */
fun getPluginsOnline(): Array<PluginData> {
return (getKey<Array<PluginData>>(PLUGINS_KEY) ?: emptyArray()).filter { !it.isDeleted }.toTypedArray()
}

// Helper for internal use to preserve tombstones
private fun getPluginsOnlineRaw(): Array<PluginData> {
return getKey<Array<PluginData>>(PLUGINS_KEY) ?: emptyArray()
}

/**
* Store data about the plugin for fetching later
* */
private suspend fun setPluginData(data: PluginData) {
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline()
val newPlugins = plugins.filter { it.filePath != data.filePath } + data
val plugins = getPluginsOnlineRaw()
// Update or Add: filter out old entry (by filePath or internalName?)
// filePath is unique per install.
// We want to keep others, and replace THIS one.
val newPlugins = plugins.filter { it.filePath != data.filePath } + data.copy(isDeleted = false, addedDate = System.currentTimeMillis())
setKey(PLUGINS_KEY, newPlugins)
} else {
val plugins = getPluginsLocal()
Expand All @@ -129,8 +147,12 @@ object PluginManager {
if (data == null) return
lock.withLock {
if (data.isOnline) {
val plugins = getPluginsOnline().filter { it.url != data.url }
setKey(PLUGINS_KEY, plugins)
val plugins = getPluginsOnlineRaw()
// Mark as deleted (Tombstone)
val newPlugins = plugins.map {
if (it.filePath == data.filePath) it.copy(isDeleted = true, addedDate = System.currentTimeMillis()) else it
}
setKey(PLUGINS_KEY, newPlugins)
} else {
val plugins = getPluginsLocal().filter { it.filePath != data.filePath }
setKey(PLUGINS_KEY_LOCAL, plugins)
Expand All @@ -140,14 +162,20 @@ object PluginManager {

suspend fun deleteRepositoryData(repositoryPath: String) {
lock.withLock {
val plugins = getPluginsOnline().filter {
!it.filePath.contains(repositoryPath)
}
val file = File(repositoryPath)
safe {
if (file.exists()) file.deleteRecursively()
val plugins = getPluginsOnlineRaw()
// Mark all plugins in this repo as deleted
val newPlugins = plugins.map {
if (it.filePath.contains(repositoryPath)) it.copy(isDeleted = true, addedDate = System.currentTimeMillis()) else it
}
setKey(PLUGINS_KEY, plugins)
// Logic to actually delete files handled by caller (removeRepository)?
// removeRepository calls: safe { file.deleteRecursively() }
// So files are gone. We just update the list.
// But removeRepository also calls unloadPlugin...

// Wait, removeRepository calls PluginManager.deleteRepositoryData(file.absolutePath)
// It also deletes the directory.
// So we just need to update the Key.
setKey(PLUGINS_KEY, newPlugins)
}
}

Expand All @@ -165,9 +193,7 @@ object PluginManager {
}


fun getPluginsOnline(): Array<PluginData> {
return getKey(PLUGINS_KEY) ?: emptyArray()
}


fun getPluginsLocal(): Array<PluginData> {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
Expand Down Expand Up @@ -360,27 +386,34 @@ object PluginManager {
}.flatten().distinctBy { it.second.url }

val providerLang = activity.getApiProviderLangSettings()
//Log.i(TAG, "providerLang => ${providerLang.toJson()}")

// Get the list of plugins that SHOULD be installed (synced from cloud)
val targetPlugins = getPluginsOnline().map { it.internalName }.toSet()

// Iterate online repos and returns not downloaded plugins
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
val sitePlugin = onlineData.second
val tvtypes = sitePlugin.tvTypes ?: listOf()

//Don't include empty urls
// Don't include empty urls
if (sitePlugin.url.isBlank()) {
return@mapNotNull null
}
if (sitePlugin.repositoryUrl.isNullOrBlank()) {
return@mapNotNull null
}

//Omit already existing plugins
// Omit already existing plugins
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) {
Log.i(TAG, "Skip > ${sitePlugin.internalName}")
return@mapNotNull null
}

// FILTER: Only download plugins that are in our synced list
if (!targetPlugins.contains(sitePlugin.internalName)) {
return@mapNotNull null
}

//Omit non-NSFW if mode is set to NSFW only
if (mode == AutoDownloadMode.NsfwOnly) {
if (!tvtypes.contains(TvType.NSFW.name)) {
Expand Down Expand Up @@ -766,9 +799,10 @@ object PluginManager {
val data = PluginData(
internalName,
pluginUrl,
true,
false, // Mark as local so it updates PLUGINS_KEY_LOCAL immediately
newFile.absolutePath,
PLUGIN_VERSION_NOT_SET
PLUGIN_VERSION_NOT_SET,
System.currentTimeMillis()
)

return if (loadPlugin) {
Expand All @@ -795,7 +829,10 @@ object PluginManager {
return try {
if (File(file.absolutePath).delete()) {
unloadPlugin(file.absolutePath)
list.forEach { deletePluginData(it) }
list.forEach {
deletePluginData(it)
FirestoreSyncManager.notifyPluginDeleted(it.internalName)
}
return true
}
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.FirestoreSyncManager
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.BufferedInputStream
Expand Down Expand Up @@ -169,7 +170,8 @@ object RepositoryManager {
repoLock.withLock {
val currentRepos = getRepositories()
// No duplicates
setKey(REPOSITORIES_KEY, (currentRepos + repository).distinctBy { it.url })
val newRepos = (currentRepos + repository).distinctBy { it.url }.toTypedArray()
setKey(REPOSITORIES_KEY, newRepos)
}
}

Expand All @@ -182,7 +184,7 @@ object RepositoryManager {
repoLock.withLock {
val currentRepos = getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
// No duplicates
val newRepos = currentRepos.filter { it.url != repository.url }
val newRepos = currentRepos.filter { it.url != repository.url }.toTypedArray()
setKey(REPOSITORIES_KEY, newRepos)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import com.lagradost.cloudstream3.utils.toKotlinObject
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import com.lagradost.cloudstream3.utils.txt
import java.net.URLEncoder
Expand Down Expand Up @@ -1137,4 +1137,4 @@ class AniListApi : SyncAPI() {
data class GetSearchRoot(
@JsonProperty("data") val data: GetSearchPage?,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import com.lagradost.cloudstream3.utils.toKotlinObject
import com.lagradost.cloudstream3.utils.txt
import java.text.SimpleDateFormat
import java.time.Instant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import com.lagradost.cloudstream3.utils.toKotlinObject
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
Expand Down Expand Up @@ -448,4 +448,4 @@ class ControllerActivity : ExpandedControllerActivity() {
SkipNextEpisodeController(skipOpButton)
)
}
}
}
Loading