mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Safer API and fixed syncing resume watching
This commit is contained in:
parent
4b7fc62237
commit
9e85359ad3
8 changed files with 257 additions and 201 deletions
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,20 +271,24 @@ 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() {
|
||||||
|
normalSafeApiCall {
|
||||||
if (!shouldUploadBackup()) {
|
if (!shouldUploadBackup()) {
|
||||||
willUploadSoon = false
|
willUploadSoon = false
|
||||||
Log.d(LOG_KEY, "${this.name}: upload not required, data is same")
|
Log.d(LOG_KEY, "${this.name}: upload not required, data is same")
|
||||||
return
|
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) {
|
||||||
|
normalSafeApiCall {
|
||||||
scheduleUpload()
|
scheduleUpload()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun upload() {
|
private fun upload() {
|
||||||
if (uploadJob != null && uploadJob!!.isActive) {
|
if (uploadJob != null && uploadJob!!.isActive) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ------ SafeBackupAPI wrappers ------
|
||||||
|
override suspend fun getIsReady(): Boolean {
|
||||||
|
return suspendSafeApiCall {
|
||||||
|
isReady()
|
||||||
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBackupAPI<LOGIN_DATA> {
|
override fun getIsLoggedIn(): Boolean {
|
||||||
data class JSONComparison(
|
return normalSafeApiCall { loginInfo() } != null
|
||||||
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<*>>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
)
|
|
||||||
|
|
||||||
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 {
|
override fun setIsUploadingSoon() {
|
||||||
var result: JSONCompareResult?
|
willUploadSoon = true
|
||||||
|
|
||||||
val executionTime = measureTimeMillis {
|
|
||||||
result = try {
|
|
||||||
JSONCompare.compareJSON(old, new, JSONCompareMode.NON_EXTENSIBLE)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val failed = result?.failed() ?: true
|
override suspend fun scheduleDownload(runNow: Boolean) {
|
||||||
Log.d(
|
suspendSafeApiCall {
|
||||||
LOG_KEY,
|
scheduleDownload(runNow, false)
|
||||||
"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()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue