Improve favorites sync statuses

This commit is contained in:
Jobobby04
2024-12-08 13:35:36 -05:00
parent 3f8cce8a32
commit 76af3b59f0
4 changed files with 243 additions and 141 deletions
@@ -64,18 +64,18 @@ class FavoritesSyncHelper(val context: Context) {
?: EHentai(0, true, context)
}
private val storage = LocalFavoritesStorage()
private val storage by lazy { LocalFavoritesStorage() }
private val galleryAdder = GalleryAdder()
private val galleryAdder by lazy { GalleryAdder() }
private val throttleManager = EHentaiThrottleManager()
private val throttleManager by lazy { EHentaiThrottleManager() }
private var wifiLock: WifiManager.WifiLock? = null
private var wakeLock: PowerManager.WakeLock? = null
private val logger = xLog()
private val logger by lazy { xLog() }
val status: MutableStateFlow<FavoritesSyncStatus> = MutableStateFlow(FavoritesSyncStatus.Idle(context))
val status: MutableStateFlow<FavoritesSyncStatus> = MutableStateFlow(FavoritesSyncStatus.Idle)
@Synchronized
fun runSync(scope: CoroutineScope) {
@@ -83,7 +83,7 @@ class FavoritesSyncHelper(val context: Context) {
return
}
status.value = FavoritesSyncStatus.Initializing(context)
status.value = FavoritesSyncStatus.Initializing
scope.launch(Dispatchers.IO) { beginSync() }
}
@@ -91,13 +91,12 @@ class FavoritesSyncHelper(val context: Context) {
private suspend fun beginSync() {
// Check if logged in
if (!prefs.enableExhentai().get()) {
status.value = FavoritesSyncStatus.Error(context.stringResource(SYMR.strings.please_login))
status.value = FavoritesSyncStatus.SyncError.NotLoggedInSyncError
return
}
// Validate library state
status.value =
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_verifying_library))
status.value = FavoritesSyncStatus.Processing.VerifyingLibrary
val libraryManga = getLibraryManga.await()
val seenManga = HashSet<Long>(libraryManga.size)
libraryManga.forEach { (manga) ->
@@ -106,7 +105,7 @@ class FavoritesSyncHelper(val context: Context) {
if (manga.id in seenManga) {
val inCategories = getCategories.await(manga.id)
status.value = FavoritesSyncStatus.BadLibraryState
.MangaInMultipleCategories(manga, inCategories, context)
.MangaInMultipleCategories(manga.id, manga.title, inCategories.map { it.name })
logger.w(context.stringResource(SYMR.strings.favorites_sync_gallery_multiple_categories_error, manga.id))
return
@@ -117,17 +116,15 @@ class FavoritesSyncHelper(val context: Context) {
// Download remote favorites
val favorites = try {
status.value =
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_downloading))
status.value = FavoritesSyncStatus.Processing.DownloadingFavorites
exh.fetchFavorites()
} catch (e: Exception) {
status.value =
FavoritesSyncStatus.Error(context.stringResource(SYMR.strings.favorites_sync_failed_to_featch))
status.value = FavoritesSyncStatus.SyncError.FailedToFetchFavorites
logger.e(context.stringResource(SYMR.strings.favorites_sync_could_not_fetch), e)
return
}
val errorList = mutableListOf<String>()
val errorList = mutableListOf<FavoritesSyncStatus.SyncError.GallerySyncError>()
try {
// Take wake + wifi locks
@@ -157,23 +154,17 @@ class FavoritesSyncHelper(val context: Context) {
// Do not update galleries while syncing favorites
EHentaiUpdateWorker.cancelBackground(context)
status.value = FavoritesSyncStatus.Processing(
context.stringResource(SYMR.strings.favorites_sync_calculating_remote_changes),
)
status.value = FavoritesSyncStatus.Processing.CalculatingRemoteChanges
val remoteChanges = storage.getChangedRemoteEntries(favorites.first)
val localChanges = if (prefs.exhReadOnlySync().get()) {
null // Do not build local changes if they are not going to be applied
} else {
status.value = FavoritesSyncStatus.Processing(
context.stringResource(SYMR.strings.favorites_sync_calculating_local_changes),
)
status.value = FavoritesSyncStatus.Processing.CalculatingLocalChanges
storage.getChangedDbEntries()
}
// Apply remote categories
status.value = FavoritesSyncStatus.Processing(
context.stringResource(SYMR.strings.favorites_sync_syncing_category_names),
)
status.value = FavoritesSyncStatus.Processing.SyncingCategoryNames
applyRemoteCategories(favorites.second)
// Apply change sets
@@ -182,8 +173,7 @@ class FavoritesSyncHelper(val context: Context) {
applyChangeSetToRemote(errorList, localChanges)
}
status.value =
FavoritesSyncStatus.Processing(context.stringResource(SYMR.strings.favorites_sync_cleaning_up))
status.value = FavoritesSyncStatus.Processing.CleaningUp
storage.snapshotEntries()
withUIContext {
@@ -194,9 +184,7 @@ class FavoritesSyncHelper(val context: Context) {
logger.w(context.stringResource(SYMR.strings.favorites_sync_ignoring_exception), e)
return
} catch (e: Exception) {
status.value = FavoritesSyncStatus.Error(
context.stringResource(SYMR.strings.favorites_sync_unknown_error, e.message.orEmpty()),
)
status.value = FavoritesSyncStatus.SyncError.UnknownSyncError(e.message.orEmpty())
logger.e(context.stringResource(SYMR.strings.favorites_sync_sync_error), e)
return
} finally {
@@ -215,7 +203,7 @@ class FavoritesSyncHelper(val context: Context) {
}
if (errorList.isEmpty()) {
status.value = FavoritesSyncStatus.Idle(context)
status.value = FavoritesSyncStatus.Idle
} else {
status.value = FavoritesSyncStatus.CompleteWithErrors(errorList)
}
@@ -249,7 +237,7 @@ class FavoritesSyncHelper(val context: Context) {
}
}
private suspend fun addGalleryRemote(errorList: MutableList<String>, gallery: FavoriteEntry) {
private suspend fun addGalleryRemote(errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>, gallery: FavoriteEntry) {
val url = "${exh.baseUrl}/gallerypopups.php?gid=${gallery.gid}&t=${gallery.token}&act=addfav"
val request = POST(
@@ -263,13 +251,16 @@ class FavoritesSyncHelper(val context: Context) {
)
if (!explicitlyRetryExhRequest(10, request)) {
val errorString = "Unable to add gallery to remote server: '${gallery.title}' (GID: ${gallery.gid})!"
val error = FavoritesSyncStatus.SyncError.GallerySyncError.UnableToAddGalleryToRemote(
gallery.title,
gallery.gid
)
if (prefs.exhLenientSync().get()) {
errorList += errorString
errorList += error
} else {
status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException(errorString)
status.value = error
throw IgnoredException(error)
}
}
}
@@ -293,12 +284,13 @@ class FavoritesSyncHelper(val context: Context) {
return success
}
private suspend fun applyChangeSetToRemote(errorList: MutableList<String>, changeSet: ChangeSet) {
private suspend fun applyChangeSetToRemote(
errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>,
changeSet: ChangeSet,
) {
// Apply removals
if (changeSet.removed.isNotEmpty()) {
status.value = FavoritesSyncStatus.Processing(
context.stringResource(SYMR.strings.favorites_sync_removing_galleries, changeSet.removed.size),
)
status.value = FavoritesSyncStatus.Processing.RemovingRemoteGalleries(changeSet.removed.size)
val formBody = FormBody.Builder()
.add("ddact", "delete")
@@ -315,13 +307,11 @@ class FavoritesSyncHelper(val context: Context) {
)
if (!explicitlyRetryExhRequest(10, request)) {
val errorString = context.stringResource(SYMR.strings.favorites_sync_unable_to_delete)
if (prefs.exhLenientSync().get()) {
errorList += errorString
errorList += FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote
} else {
status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException(errorString)
status.value = FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote
throw IgnoredException(FavoritesSyncStatus.SyncError.GallerySyncError.UnableToDeleteFromRemote)
}
}
}
@@ -329,10 +319,10 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions
throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it ->
status.value = FavoritesSyncStatus.Processing(
message = context.stringResource(SYMR.strings.favorites_sync_adding_to_remote, index + 1, changeSet.added.size),
isThrottle = needWarnThrottle(),
context = context,
status.value = FavoritesSyncStatus.Processing.AddingGalleryToRemote(
index = index + 1,
total = changeSet.added.size,
isThrottling = needWarnThrottle(),
title = it.title,
)
@@ -342,14 +332,17 @@ class FavoritesSyncHelper(val context: Context) {
}
}
private suspend fun applyChangeSetToLocal(errorList: MutableList<String>, changeSet: ChangeSet) {
private suspend fun applyChangeSetToLocal(
errorList: MutableList<FavoritesSyncStatus.SyncError.GallerySyncError>,
changeSet: ChangeSet,
) {
val removedManga = mutableListOf<Manga>()
// Apply removals
changeSet.removed.forEachIndexed { index, it ->
status.value = FavoritesSyncStatus.Processing(
context.stringResource(SYMR.strings.favorites_sync_remove_from_local, index + 1, changeSet.removed.size),
title = it.title,
status.value = FavoritesSyncStatus.Processing.RemovingGalleryFromLocal(
index = index + 1,
total = changeSet.removed.size,
)
val url = it.getUrl()
@@ -379,10 +372,10 @@ class FavoritesSyncHelper(val context: Context) {
// Apply additions
throttleManager.resetThrottle()
changeSet.added.forEachIndexed { index, it ->
status.value = FavoritesSyncStatus.Processing(
message = context.stringResource(SYMR.strings.favorites_sync_add_to_local, index + 1, changeSet.added.size),
isThrottle = needWarnThrottle(),
context = context,
status.value = FavoritesSyncStatus.Processing.AddingGalleryToLocal(
index = index + 1,
total = changeSet.added.size,
isThrottling = needWarnThrottle(),
title = it.title,
)
@@ -405,24 +398,23 @@ class FavoritesSyncHelper(val context: Context) {
return@forEachIndexed
}
val errorString = context.stringResource(SYMR.strings.favorites_sync_failed_to_add_to_local) +
when (result) {
is GalleryAddEvent.Fail.Error -> context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_error, it.title, result.logMessage,
)
is GalleryAddEvent.Fail.UnknownType -> context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl,
)
is GalleryAddEvent.Fail.UnknownSource -> context.stringResource(
SYMR.strings.favorites_sync_failed_to_add_to_local_unknown_type, it.title, result.galleryUrl,
)
}
val error = when (result) {
is GalleryAddEvent.Fail.Error -> FavoritesSyncStatus.SyncError.GallerySyncError.GalleryAddFail(
it.title, result.logMessage,
)
is GalleryAddEvent.Fail.UnknownType -> FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail(
it.title, result.galleryUrl,
)
is GalleryAddEvent.Fail.UnknownSource -> FavoritesSyncStatus.SyncError.GallerySyncError.InvalidGalleryFail(
it.title, result.galleryUrl,
)
}
if (prefs.exhLenientSync().get()) {
errorList += errorString
errorList += error
} else {
status.value = FavoritesSyncStatus.Error(errorString)
throw IgnoredException(errorString)
status.value = error
throw IgnoredException(error)
}
} else if (result is GalleryAddEvent.Success) {
insertedMangaCategories += categories[it.category].id to result.manga
@@ -438,59 +430,74 @@ class FavoritesSyncHelper(val context: Context) {
private fun needWarnThrottle() =
throttleManager.throttleTime >= THROTTLE_WARN
class IgnoredException(message: String) : RuntimeException(message)
class IgnoredException(message: FavoritesSyncStatus.SyncError.GallerySyncError) : RuntimeException(message.toString())
companion object {
private val THROTTLE_WARN = 1.seconds
}
}
@Serializable
sealed class FavoritesSyncStatus {
abstract val message: String
data class Error(override val message: String) : FavoritesSyncStatus()
data class Idle(override val message: String) : FavoritesSyncStatus() {
constructor(context: Context) : this(context.stringResource(SYMR.strings.favorites_sync_waiting_for_start))
}
sealed class BadLibraryState : FavoritesSyncStatus() {
data class MangaInMultipleCategories(
val manga: Manga,
val categories: List<Category>,
override val message: String,
) : BadLibraryState() {
constructor(manga: Manga, categories: List<Category>, context: Context) :
this(
manga = manga,
categories = categories,
message = context.stringResource(
SYMR.strings.favorites_sync_gallery_in_multiple_categories, manga.title,
categories.joinToString {
it.name
},
),
)
@Serializable
sealed class SyncError : FavoritesSyncStatus() {
@Serializable
data object NotLoggedInSyncError : SyncError()
@Serializable
data object FailedToFetchFavorites : SyncError()
@Serializable
data class UnknownSyncError(val message: String) : SyncError()
@Serializable
sealed class GallerySyncError : SyncError() {
@Serializable
data class UnableToAddGalleryToRemote(val title: String, val gid: String): GallerySyncError()
@Serializable
data object UnableToDeleteFromRemote : GallerySyncError()
@Serializable
data class GalleryAddFail(val title: String, val reason: String): GallerySyncError()
@Serializable
data class InvalidGalleryFail(val title: String, val url: String): GallerySyncError()
}
}
data class Initializing(override val message: String) : FavoritesSyncStatus() {
constructor(context: Context) : this(context.stringResource(SYMR.strings.favorites_sync_initializing))
@Serializable
data object Idle : FavoritesSyncStatus()
@Serializable
sealed class BadLibraryState : FavoritesSyncStatus() {
@Serializable
data class MangaInMultipleCategories(
val mangaId: Long,
val mangaTitle: String,
val categories: List<String>,
) : BadLibraryState()
}
data class Processing(
override val message: String,
val title: String? = null,
) : FavoritesSyncStatus() {
constructor(message: String, isThrottle: Boolean, context: Context, title: String?) :
this(
if (isThrottle) {
context.stringResource(SYMR.strings.favorites_sync_processing_throttle, message)
} else {
message
},
title,
)
val delayedMessage get() = if (title != null) this.message + "\n\n" + title else null
}
data class CompleteWithErrors(val messages: List<String>) : FavoritesSyncStatus() {
override val message: String = messages.joinToString("\n")
@Serializable
data object Initializing : FavoritesSyncStatus()
@Serializable
sealed class Processing : FavoritesSyncStatus() {
data object VerifyingLibrary : Processing()
data object DownloadingFavorites : Processing()
data object CalculatingRemoteChanges : Processing()
data object CalculatingLocalChanges : Processing()
data object SyncingCategoryNames : Processing()
data class RemovingRemoteGalleries(val galleryCount: Int) : Processing()
data class AddingGalleryToRemote(
val index: Int,
val total: Int,
val isThrottling: Boolean,
val title: String,
) : Processing()
data class RemovingGalleryFromLocal(
val index: Int,
val total: Int,
) : Processing()
data class AddingGalleryToLocal(
val index: Int,
val total: Int,
val isThrottling: Boolean,
val title: String,
) : Processing()
data object CleaningUp : Processing()
}
@Serializable
data class CompleteWithErrors(val messages: List<SyncError.GallerySyncError>) : FavoritesSyncStatus()
}