Safer API and fixed syncing resume watching

This commit is contained in:
CranberrySoup 2023-10-31 21:36:15 +01:00
parent 4b7fc62237
commit 9e85359ad3
8 changed files with 257 additions and 201 deletions

View file

@ -44,7 +44,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// used for active backup // used for active backup
val BackupApis val BackupApis
get() = listOf<BackupAPI<*>>( get() = listOf<SafeBackupAPI>(
googleDriveApi, pcloudApi googleDriveApi, pcloudApi
) )

View file

@ -7,9 +7,8 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.LOG_KEY import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.compareJson import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.mergeBackup
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BackupUtils.getBackup import com.lagradost.cloudstream3.utils.BackupUtils.getBackup
@ -22,7 +21,7 @@ import org.skyscreamer.jsonassert.JSONCompare
import org.skyscreamer.jsonassert.JSONCompareMode import org.skyscreamer.jsonassert.JSONCompareMode
import org.skyscreamer.jsonassert.JSONCompareResult import org.skyscreamer.jsonassert.JSONCompareResult
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.minutes
interface RemoteFile { interface RemoteFile {
class Error(val message: String? = null, val throwable: Throwable? = null) : RemoteFile class Error(val message: String? = null, val throwable: Throwable? = null) : RemoteFile
@ -30,8 +29,83 @@ interface RemoteFile {
class Success(val remoteData: String) : RemoteFile class Success(val remoteData: String) : RemoteFile
} }
/**
* Safe wrapper for the backup api to be used outside the class without fear
* of causing crashes.
*/
interface SafeBackupAPI {
/**
* @return true if the service is ready for uploads and downloads.
* This includes a login check.
*/
suspend fun getIsReady(): Boolean
suspend fun scheduleDownload(runNow: Boolean = false)
fun getIsLoggedIn(): Boolean
fun scheduleUpload()
fun scheduleUpload(changedKey: String, isSettings: Boolean)
/**
* Warns the service that an upload is incoming. Used to prevent simultaneous download and upload.
*/
fun setIsUploadingSoon()
}
/**
* Easy interface to implement remote sync by only implementing the download and upload part of the service.
* @see BackupAPI for how the methods get used
*/
interface IBackupAPI<LOGIN_DATA> {
/**
* Gets the user login info for uploading and downloading the backup.
* If null no backup or download will be run.
*/
suspend fun getLoginData(): LOGIN_DATA?
/**
* Additional check if the backup operation should be run.
* Return false here to deny any backup work.
*/
suspend fun isReady(): Boolean = true
/**
* Get the backup file as a string from the remote storage.
* @see RemoteFile.Success
* @see RemoteFile.Error
* @see RemoteFile.NotFound
*/
suspend fun getRemoteFile(context: Context, loginData: LOGIN_DATA): RemoteFile
suspend fun uploadFile(
context: Context,
backupJson: String,
loginData: LOGIN_DATA
)
}
/**
* Helper class for the IBackupAPI which implements a scheduler, logging and checks.
* This makes the individual backup service implementations easier and more lightweight.
*/
abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>, abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
AccountManager(defIndex) { AccountManager(defIndex), SafeBackupAPI {
data class JSONComparison(
val failed: Boolean,
val result: JSONCompareResult?
)
data class PreferencesSchedulerData<T>(
val syncPrefs: SharedPreferences,
val storeKey: String,
val oldValue: T,
val newValue: T,
val source: BackupUtils.RestoreSource
)
data class SharedPreferencesWithListener(
val self: SharedPreferences,
val scheduler: Scheduler<PreferencesSchedulerData<*>>
)
companion object { companion object {
const val LOG_KEY = "BACKUP" const val LOG_KEY = "BACKUP"
@ -39,8 +113,118 @@ abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
// cloud project per user so there is no way to hit quota. Later we should implement // cloud project per user so there is no way to hit quota. Later we should implement
// some kind of adaptive throttling which will increase decrease throttle time based // some kind of adaptive throttling which will increase decrease throttle time based
// on factors like: live devices, quota limits, etc // on factors like: live devices, quota limits, etc
val UPLOAD_THROTTLE = 30.seconds val UPLOAD_THROTTLE = 5.minutes
val DOWNLOAD_THROTTLE = 120.seconds val DOWNLOAD_THROTTLE = 5.minutes
const val SYNC_HISTORY_PREFIX = "_hs/"
fun SharedPreferences.logHistoryChanged(
path: String,
source: BackupUtils.RestoreSource
) {
edit().putLong("${source.syncPrefix}$path", System.currentTimeMillis()).apply()
}
fun compareJson(old: String, new: String): JSONComparison {
var result: JSONCompareResult?
val executionTime = measureTimeMillis {
result = try {
JSONCompare.compareJSON(old, new, JSONCompareMode.NON_EXTENSIBLE)
} catch (e: Exception) {
null
}
}
val failed = result?.failed() ?: true
Log.d(
LOG_KEY,
"JSON comparison took $executionTime ms, compareFailed=$failed, result=$result"
)
return JSONComparison(failed, result)
}
private fun getSyncKeys(data: BackupUtils.BackupFile) =
data.syncMeta._Long.orEmpty().filter { it.key.startsWith(SYNC_HISTORY_PREFIX) }
/**
* Merges the backup data with the app data.
* @param overwrite if true it overwrites all data same as restoring from a backup.
* if false it only updates outdated keys. Should be true on first initialization.
*/
private fun mergeBackup(context: Context, incomingData: String, overwrite: Boolean) {
val newData = DataStore.mapper.readValue<BackupUtils.BackupFile>(incomingData)
if (overwrite) {
Log.d(LOG_KEY, "overwriting data")
context.restore(newData)
return
}
val keysToUpdate = getKeysToUpdate(context.getBackup(), newData)
if (keysToUpdate.isEmpty()) {
Log.d(LOG_KEY, "remote data is up to date, sync not needed")
return
}
Log.d(LOG_KEY, incomingData)
context.restore(newData, keysToUpdate)
}
private fun getKeysToUpdate(
currentData: BackupUtils.BackupFile,
newData: BackupUtils.BackupFile
): Set<String> {
val currentSync = getSyncKeys(currentData)
val newSync = getSyncKeys(newData)
val changedKeys = newSync.filter {
val localTimestamp = currentSync[it.key] ?: 0L
it.value > localTimestamp
}.keys
val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) }
val missingKeys = getAllMissingKeys(currentData, newData)
return (missingKeys + onlyLocalKeys + changedKeys).toSet()
}
private fun getAllMissingKeys(
old: BackupUtils.BackupFile,
new: BackupUtils.BackupFile
): List<String> = BackupUtils.RestoreSource
.values()
.filter { it != BackupUtils.RestoreSource.SYNC }
.fold(mutableListOf()) { acc, source ->
acc.addAll(getMissingKeysPrefixed(source, old, new))
acc
}
private fun getMissingKeysPrefixed(
restoreSource: BackupUtils.RestoreSource,
old: BackupUtils.BackupFile,
new: BackupUtils.BackupFile
): List<String> {
val oldSource = old.getData(restoreSource)
val newSource = new.getData(restoreSource)
val prefixToMatch = restoreSource.syncPrefix
return listOf(
*getMissing(oldSource._Bool, newSource._Bool),
*getMissing(oldSource._Long, newSource._Long),
*getMissing(oldSource._Float, newSource._Float),
*getMissing(oldSource._Int, newSource._Int),
*getMissing(oldSource._String, newSource._String),
*getMissing(oldSource._StringSet, newSource._StringSet),
).map {
prefixToMatch + it
}
}
private fun getMissing(old: Map<String, *>?, new: Map<String, *>?): Array<String> =
(new.orEmpty().keys - old.orEmpty().keys).toTypedArray()
} }
/** /**
@ -77,7 +261,7 @@ abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
} }
} }
var willUploadSoon: Boolean? = null private var willUploadSoon: Boolean? = null
private var uploadJob: Job? = null private var uploadJob: Job? = null
private fun shouldUploadBackup(): Boolean { private fun shouldUploadBackup(): Boolean {
@ -87,19 +271,23 @@ abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
return compareJson(lastBackupJson ?: "", newBackup).failed return compareJson(lastBackupJson ?: "", newBackup).failed
} }
fun scheduleUpload() { override fun scheduleUpload() {
if (!shouldUploadBackup()) { normalSafeApiCall {
willUploadSoon = false if (!shouldUploadBackup()) {
Log.d(LOG_KEY, "${this.name}: upload not required, data is same") willUploadSoon = false
return Log.d(LOG_KEY, "${this.name}: upload not required, data is same")
} return@normalSafeApiCall
}
upload() upload()
}
} }
// changedKey and isSettings is currently unused, might be useful for more efficient update checker. // changedKey and isSettings is currently unused, might be useful for more efficient update checker.
fun scheduleUpload(changedKey: String, isSettings: Boolean) { override fun scheduleUpload(changedKey: String, isSettings: Boolean) {
scheduleUpload() normalSafeApiCall {
scheduleUpload()
}
} }
private fun upload() { private fun upload() {
@ -170,7 +358,10 @@ abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
} }
is RemoteFile.Error -> { is RemoteFile.Error -> {
Log.d(LOG_KEY, "${this.name}: getRemoteFile failed with message: ${remoteFile.message}.") Log.d(
LOG_KEY,
"${this.name}: getRemoteFile failed with message: ${remoteFile.message}."
)
remoteFile.throwable?.let { error -> logError(error) } remoteFile.throwable?.let { error -> logError(error) }
null null
} }
@ -195,162 +386,26 @@ abstract class BackupAPI<LOGIN_DATA>(defIndex: Int) : IBackupAPI<LOGIN_DATA>,
lastBackupJson = remoteData lastBackupJson = remoteData
mergeBackup(context, remoteData, overwrite) mergeBackup(context, remoteData, overwrite)
} }
}
interface IBackupAPI<LOGIN_DATA> {
data class JSONComparison(
val failed: Boolean,
val result: JSONCompareResult?
)
data class PreferencesSchedulerData<T>( // ------ SafeBackupAPI wrappers ------
val syncPrefs: SharedPreferences, override suspend fun getIsReady(): Boolean {
val storeKey: String, return suspendSafeApiCall {
val oldValue: T, isReady()
val newValue: T, } ?: false
val source: BackupUtils.RestoreSource }
)
data class SharedPreferencesWithListener( override fun getIsLoggedIn(): Boolean {
val self: SharedPreferences, return normalSafeApiCall { loginInfo() } != null
val scheduler: Scheduler<PreferencesSchedulerData<*>> }
)
/** override fun setIsUploadingSoon() {
* Gets the user login info for uploading and downloading the backup. willUploadSoon = true
* If null no backup or download will be run. }
*/
suspend fun getLoginData(): LOGIN_DATA?
/** override suspend fun scheduleDownload(runNow: Boolean) {
* Additional check if the backup operation should be run. suspendSafeApiCall {
* Return false here to deny any backup work. scheduleDownload(runNow, false)
*/
suspend fun isReady(): Boolean = true
/**
* Get the backup file as a string from the remote storage.
* @see RemoteFile.Success
* @see RemoteFile.Error
* @see RemoteFile.NotFound
*/
suspend fun getRemoteFile(context: Context, loginData: LOGIN_DATA): RemoteFile
suspend fun uploadFile(
context: Context,
backupJson: String,
loginData: LOGIN_DATA
)
companion object {
const val SYNC_HISTORY_PREFIX = "_hs/"
fun SharedPreferences.logHistoryChanged(path: String, source: BackupUtils.RestoreSource) {
edit().putLong("${source.syncPrefix}$path", System.currentTimeMillis())
.apply()
} }
fun compareJson(old: String, new: String): JSONComparison {
var result: JSONCompareResult?
val executionTime = measureTimeMillis {
result = try {
JSONCompare.compareJSON(old, new, JSONCompareMode.NON_EXTENSIBLE)
} catch (e: Exception) {
null
}
}
val failed = result?.failed() ?: true
Log.d(
LOG_KEY,
"JSON comparison took $executionTime ms, compareFailed=$failed, result=$result"
)
return JSONComparison(failed, result)
}
private fun getSyncKeys(data: BackupUtils.BackupFile) =
data.syncMeta._Long.orEmpty().filter { it.key.startsWith(SYNC_HISTORY_PREFIX) }
/**
* Merges the backup data with the app data.
* @param overwrite if true it overwrites all data same as restoring from a backup.
* if false it only updates outdated keys. Should be true on first initialization.
*/
fun mergeBackup(context: Context, incomingData: String, overwrite: Boolean) {
val newData = DataStore.mapper.readValue<BackupUtils.BackupFile>(incomingData)
if (overwrite) {
Log.d(LOG_KEY, "overwriting data")
context.restore(newData)
return
}
val keysToUpdate = getKeysToUpdate(context.getBackup(), newData)
if (keysToUpdate.isEmpty()) {
Log.d(LOG_KEY, "remote data is up to date, sync not needed")
return
}
Log.d(LOG_KEY, incomingData)
context.restore(newData, keysToUpdate)
}
private fun getKeysToUpdate(
currentData: BackupUtils.BackupFile,
newData: BackupUtils.BackupFile
): Set<String> {
val currentSync = getSyncKeys(currentData)
val newSync = getSyncKeys(newData)
val changedKeys = newSync.filter {
val localTimestamp = currentSync[it.key] ?: 0L
it.value > localTimestamp
}.keys
val onlyLocalKeys = currentSync.keys.filter { !newSync.containsKey(it) }
val missingKeys = getAllMissingKeys(currentData, newData)
return (missingKeys + onlyLocalKeys + changedKeys).toSet()
}
private fun getAllMissingKeys(
old: BackupUtils.BackupFile,
new: BackupUtils.BackupFile
): List<String> = BackupUtils.RestoreSource
.values()
.filter { it != BackupUtils.RestoreSource.SYNC }
.fold(mutableListOf()) { acc, source ->
acc.addAll(getMissingKeysPrefixed(source, old, new))
acc
}
private fun getMissingKeysPrefixed(
restoreSource: BackupUtils.RestoreSource,
old: BackupUtils.BackupFile,
new: BackupUtils.BackupFile
): List<String> {
val oldSource = old.getData(restoreSource)
val newSource = new.getData(restoreSource)
val prefixToMatch = restoreSource.syncPrefix
return listOf(
*getMissing(oldSource._Bool, newSource._Bool),
*getMissing(oldSource._Long, newSource._Long),
*getMissing(oldSource._Float, newSource._Float),
*getMissing(oldSource._Int, newSource._Int),
*getMissing(oldSource._String, newSource._String),
*getMissing(oldSource._StringSet, newSource._StringSet),
).map {
prefixToMatch + it
}
}
private fun getMissing(old: Map<String, *>?, new: Map<String, *>?): Array<String> =
(new.orEmpty().keys - old.orEmpty().keys)
.toTypedArray()
} }
} }

View file

@ -85,9 +85,9 @@ class GoogleDriveApi(index: Int) :
accountId, key.value, value accountId, key.value, value
) )
private fun clearValue(key: K) = removeKey(accountId, key.value) private fun clearValue(key: K) = removeKey(accountId, key.value)
private inline fun <reified T : Any> getValue(key: K) = getKey<T>( private inline fun <reified T : Any> getValue(key: K) = getKey<T>(
accountId, key.value accountId, key.value
) )

View file

@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.googleDriveApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.googleDriveApi
@ -150,7 +151,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
title = title =
getString(R.string.login_format).format(api.name, getString(R.string.account)) getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { setOnPreferenceClickListener {
val info = api.loginInfo() val info = normalSafeApiCall { api.loginInfo() }
if (info != null) { if (info != null) {
showLoginInfo(activity, api, info) showLoginInfo(activity, api, info)
} else { } else {

View file

@ -24,9 +24,9 @@ import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.BackupApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -62,7 +62,8 @@ class SettingsFragment : Fragment() {
fun Fragment?.setUpToolbar(title: String) { fun Fragment?.setUpToolbar(title: String) {
if (this == null) return if (this == null) return
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return val settingsToolbar =
view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply { settingsToolbar.apply {
setTitle(title) setTitle(title)
@ -76,7 +77,8 @@ class SettingsFragment : Fragment() {
fun Fragment?.setUpToolbar(@StringRes title: Int) { fun Fragment?.setUpToolbar(@StringRes title: Int) {
if (this == null) return if (this == null) return
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return val settingsToolbar =
view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply { settingsToolbar.apply {
setTitle(title) setTitle(title)
@ -213,7 +215,7 @@ class SettingsFragment : Fragment() {
// Only show the button if the api does not require login, requires login, but the user is logged in // Only show the button if the api does not require login, requires login, but the user is logged in
forceSyncDataBtt.isVisible = BackupApis.any { api -> forceSyncDataBtt.isVisible = BackupApis.any { api ->
api !is AuthAPI || api.loginInfo() != null api.getIsLoggedIn()
} }
forceSyncDataBtt.setOnClickListener { forceSyncDataBtt.setOnClickListener {
@ -223,7 +225,8 @@ class SettingsFragment : Fragment() {
showToast(activity, txt(R.string.syncing_data), Toast.LENGTH_SHORT) showToast(activity, txt(R.string.syncing_data), Toast.LENGTH_SHORT)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
forceSyncDataBtt.tooltipText = txt(R.string.sync_data).asString(forceSyncDataBtt.context) forceSyncDataBtt.tooltipText =
txt(R.string.sync_data).asString(forceSyncDataBtt.context)
} }
// Default focus on TV // Default focus on TV

View file

@ -54,7 +54,7 @@ object BackupUtils {
DATA, SETTINGS, SYNC; DATA, SETTINGS, SYNC;
val prefix = "$name/" val prefix = "$name/"
val syncPrefix = "${IBackupAPI.SYNC_HISTORY_PREFIX}$prefix" val syncPrefix = "${BackupAPI.SYNC_HISTORY_PREFIX}$prefix"
} }
/** /**
@ -80,7 +80,6 @@ object BackupUtils {
OPEN_SUBTITLES_USER_KEY, OPEN_SUBTITLES_USER_KEY,
"nginx_user", // Nginx user key "nginx_user", // Nginx user key
DOWNLOAD_HEADER_CACHE,
DOWNLOAD_EPISODE_CACHE DOWNLOAD_EPISODE_CACHE
) )
@ -329,7 +328,7 @@ object BackupUtils {
var prefixToRemove = prefixToMatch var prefixToRemove = prefixToMatch
if (restoreSource == RestoreSource.SYNC) { if (restoreSource == RestoreSource.SYNC) {
prefixToMatch = IBackupAPI.SYNC_HISTORY_PREFIX prefixToMatch = BackupAPI.SYNC_HISTORY_PREFIX
prefixToRemove = "" prefixToRemove = ""
} }

View file

@ -10,9 +10,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKeyClass
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKeyClass import com.lagradost.cloudstream3.AcraApplication.Companion.setKeyClass
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.BackupAPI
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import com.lagradost.cloudstream3.syncproviders.IBackupAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
const val DOWNLOAD_HEADER_CACHE = "download_header_cache" const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -149,7 +149,7 @@ object DataStore {
ioSafe { ioSafe {
backupScheduler.work( backupScheduler.work(
IBackupAPI.PreferencesSchedulerData( BackupAPI.PreferencesSchedulerData(
getSyncPrefs(), getSyncPrefs(),
path, path,
oldValueExists, oldValueExists,
@ -184,7 +184,7 @@ object DataStore {
ioSafe { ioSafe {
backupScheduler.work( backupScheduler.work(
IBackupAPI.PreferencesSchedulerData( BackupAPI.PreferencesSchedulerData(
getSyncPrefs(), getSyncPrefs(),
path, path,
oldValue, oldValue,

View file

@ -6,14 +6,12 @@ import android.os.Looper
import android.util.Log import android.util.Log
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.BackupAPI import com.lagradost.cloudstream3.syncproviders.BackupAPI
import com.lagradost.cloudstream3.syncproviders.IBackupAPI import com.lagradost.cloudstream3.syncproviders.BackupAPI.Companion.logHistoryChanged
import com.lagradost.cloudstream3.syncproviders.IBackupAPI.Companion.logHistoryChanged
import com.lagradost.cloudstream3.ui.home.HOME_BOOKMARK_VALUE_LIST import com.lagradost.cloudstream3.ui.home.HOME_BOOKMARK_VALUE_LIST
import com.lagradost.cloudstream3.ui.player.PLAYBACK_SPEED_KEY import com.lagradost.cloudstream3.ui.player.PLAYBACK_SPEED_KEY
import com.lagradost.cloudstream3.ui.player.RESIZE_MODE_KEY import com.lagradost.cloudstream3.ui.player.RESIZE_MODE_KEY
import com.lagradost.cloudstream3.utils.BackupUtils.nonTransferableKeys import com.lagradost.cloudstream3.utils.BackupUtils.nonTransferableKeys
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.runBlocking
class Scheduler<INPUT>( class Scheduler<INPUT>(
private val throttleTimeMs: Long, private val throttleTimeMs: Long,
@ -40,7 +38,7 @@ class Scheduler<INPUT>(
Regex("""^\d+/$RESULT_DUB/"""), Regex("""^\d+/$RESULT_DUB/"""),
) )
fun createBackupScheduler() = Scheduler<IBackupAPI.PreferencesSchedulerData<*>>( fun createBackupScheduler() = Scheduler<BackupAPI.PreferencesSchedulerData<*>>(
BackupAPI.UPLOAD_THROTTLE.inWholeMilliseconds, BackupAPI.UPLOAD_THROTTLE.inWholeMilliseconds,
onWork = { input -> onWork = { input ->
AccountManager.BackupApis.forEach { api -> AccountManager.BackupApis.forEach { api ->
@ -52,13 +50,13 @@ class Scheduler<INPUT>(
}, },
beforeWork = { _ -> beforeWork = { _ ->
AccountManager.BackupApis.filter { api -> AccountManager.BackupApis.filter { api ->
api.isReady() api.getIsReady()
}.forEach { }.forEach {
it.willUploadSoon = true it.setIsUploadingSoon()
} }
}, },
canWork = { input -> canWork = { input ->
val hasSomeActiveManagers = AccountManager.BackupApis.any { it.isReady() } val hasSomeActiveManagers = AccountManager.BackupApis.any { it.getIsReady() }
if (!hasSomeActiveManagers) { if (!hasSomeActiveManagers) {
return@Scheduler false return@Scheduler false
} }
@ -97,14 +95,14 @@ class Scheduler<INPUT>(
fun SharedPreferences.attachBackupListener( fun SharedPreferences.attachBackupListener(
source: BackupUtils.RestoreSource = BackupUtils.RestoreSource.SETTINGS, source: BackupUtils.RestoreSource = BackupUtils.RestoreSource.SETTINGS,
syncPrefs: SharedPreferences syncPrefs: SharedPreferences
): IBackupAPI.SharedPreferencesWithListener { ): BackupAPI.SharedPreferencesWithListener {
val scheduler = createBackupScheduler() val scheduler = createBackupScheduler()
var lastValue = all var lastValue = all
registerOnSharedPreferenceChangeListener { sharedPreferences, storeKey -> registerOnSharedPreferenceChangeListener { sharedPreferences, storeKey ->
ioSafe { ioSafe {
scheduler.work( scheduler.work(
IBackupAPI.PreferencesSchedulerData( BackupAPI.PreferencesSchedulerData(
syncPrefs, syncPrefs,
storeKey, storeKey,
lastValue[storeKey], lastValue[storeKey],
@ -116,10 +114,10 @@ class Scheduler<INPUT>(
lastValue = sharedPreferences.all lastValue = sharedPreferences.all
} }
return IBackupAPI.SharedPreferencesWithListener(this, scheduler) return BackupAPI.SharedPreferencesWithListener(this, scheduler)
} }
fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences): IBackupAPI.SharedPreferencesWithListener { fun SharedPreferences.attachBackupListener(syncPrefs: SharedPreferences): BackupAPI.SharedPreferencesWithListener {
return attachBackupListener(BackupUtils.RestoreSource.SETTINGS, syncPrefs) return attachBackupListener(BackupUtils.RestoreSource.SETTINGS, syncPrefs)
} }
} }
@ -172,7 +170,7 @@ class Scheduler<INPUT>(
runnable = Runnable { runnable = Runnable {
Log.d(BackupAPI.LOG_KEY, "[$id] schedule success") Log.d(BackupAPI.LOG_KEY, "[$id] schedule success")
runBlocking { ioSafe {
onWork(input) onWork(input)
} }
}.also { run -> }.also { run ->