diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt index 7e5643d4e..d23950b8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -67,11 +67,15 @@ class Tracker @Inject constructor( return result } + suspend fun getTracks(ids: Set): List { + return getAllTracks().filterTo(ArrayList(ids.size)) { x -> x.tracking.manga.id in ids } + } + suspend fun gc() { repository.gc() } - suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates { + suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates.Success { val manga = mangaRepositoryFactory.create(track.manga.source).getDetails(track.manga) val updates = compare(track, manga, getBranch(manga)) if (commit) { @@ -103,24 +107,24 @@ class Tracker @Inject constructor( /** * The main functionality of tracker: check new chapters in [manga] comparing to the [track] */ - private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates { + private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success { if (track.isEmpty()) { // first check or manga was empty on last check - return MangaUpdates(manga, emptyList(), isValid = false) + return MangaUpdates.Success(manga, emptyList(), isValid = false) } val chapters = requireNotNull(manga.getChapters(branch)) val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId } return when { newChapters.isEmpty() -> { - MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) + MangaUpdates.Success(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId) } newChapters.size == chapters.size -> { - MangaUpdates(manga, emptyList(), isValid = false) + MangaUpdates.Success(manga, emptyList(), isValid = false) } else -> { - MangaUpdates(manga, newChapters, isValid = true) + MangaUpdates.Success(manga, newChapters, isValid = true) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 8f4eff796..8114c5159 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -118,7 +118,7 @@ class TrackingRepository @Inject constructor( db.trackLogsDao.gc() } - suspend fun saveUpdates(updates: MangaUpdates) { + suspend fun saveUpdates(updates: MangaUpdates.Success) { db.withTransaction { val track = getOrCreateTrack(updates.manga.id).mergeWith(updates) db.tracksDao.upsert(track) @@ -199,7 +199,7 @@ class TrackingRepository @Inject constructor( ) } - private suspend fun updatePercent(updates: MangaUpdates) { + private suspend fun updatePercent(updates: MangaUpdates.Success) { val history = db.historyDao.find(updates.manga.id) ?: return val chapters = updates.manga.chapters if (chapters.isNullOrEmpty()) { @@ -214,7 +214,7 @@ class TrackingRepository @Inject constructor( db.historyDao.update(history.copy(percent = newPercent)) } - private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity { + private fun TrackEntity.mergeWith(updates: MangaUpdates.Success): TrackEntity { val chapters = updates.manga.chapters.orEmpty() return TrackEntity( mangaId = mangaId, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt index 937f06808..b1c2da4a5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/model/MangaUpdates.kt @@ -1,13 +1,27 @@ package org.koitharu.kotatsu.tracker.domain.model +import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter -class MangaUpdates( - val manga: Manga, - val newChapters: List, - val isValid: Boolean, -) { +sealed interface MangaUpdates { - fun isNotEmpty() = newChapters.isNotEmpty() -} \ No newline at end of file + val manga: Manga + + class Success( + override val manga: Manga, + val newChapters: List, + val isValid: Boolean, + ) : MangaUpdates { + + fun isNotEmpty() = newChapters.isNotEmpty() + } + + class Failure( + override val manga: Manga, + val error: Throwable?, + ) : MangaUpdates { + + fun shouldRetry() = error is TooManyRequestExceptions + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 660ee1dc1..50c6cf8be 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat.VISIBILITY_SECRET import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.hilt.work.HiltWorker import androidx.lifecycle.asFlow import androidx.work.BackoffPolicy @@ -32,12 +33,14 @@ import coil.request.ImageRequest import dagger.Reusable import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext @@ -54,6 +57,7 @@ import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.tracker.domain.Tracker @@ -75,7 +79,7 @@ class TrackWorker @AssistedInject constructor( override suspend fun doWork(): Result { trySetForeground() - logger.log("doWork()") + logger.log("doWork(): attempt $runAttemptCount") return try { doWorkImpl() } catch (e: Throwable) { @@ -93,7 +97,12 @@ class TrackWorker @AssistedInject constructor( if (!settings.isTrackerEnabled) { return Result.success(workDataOf(0, 0)) } - val tracks = tracker.getAllTracks() + val retryIds = getRetryIds() + val tracks = if (retryIds.isNotEmpty()) { + tracker.getTracks(retryIds) + } else { + tracker.getAllTracks() + } logger.log("Total ${tracks.size} tracks") if (tracks.isEmpty()) { return Result.success(workDataOf(0, 0)) @@ -104,23 +113,32 @@ class TrackWorker @AssistedInject constructor( var success = 0 var failed = 0 + val retry = HashSet() results.forEach { x -> - if (x == null) { - failed++ - } else { - success++ + when (x) { + is MangaUpdates.Success -> success++ + is MangaUpdates.Failure -> { + failed++ + if (x.shouldRetry()) { + retry += x.manga.id + } + } } } - logger.log("Result: success: $success, failed: $failed") + if (runAttemptCount > MAX_ATTEMPTS) { + retry.clear() + } + setRetryIds(retry) + logger.log("Result: success: $success, failed: $failed, retry: ${retry.size}") val resultData = workDataOf(success, failed) - return if (success == 0 && failed != 0) { - Result.failure(resultData) - } else { - Result.success(resultData) + return when { + retry.isNotEmpty() -> Result.retry() + success == 0 && failed != 0 -> Result.failure(resultData) + else -> Result.success(resultData) } } - private suspend fun checkUpdatesAsync(tracks: List): List { + private suspend fun checkUpdatesAsync(tracks: List): List { val semaphore = Semaphore(MAX_PARALLELISM) return channelFlow { for ((track, channelId) in tracks) { @@ -142,7 +160,12 @@ class TrackWorker @AssistedInject constructor( newChapters = updates.newChapters, ) } - }.getOrNull(), + }.getOrElse { error -> + MangaUpdates.Failure( + manga = track.manga, + error = error, + ) + }, ) } } @@ -238,6 +261,22 @@ class TrackWorker @AssistedInject constructor( return ForegroundInfo(WORKER_NOTIFICATION_ID, notification) } + private suspend fun setRetryIds(ids: Set) = runInterruptible(Dispatchers.IO) { + val prefs = applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE) + prefs.edit(commit = true) { + if (ids.isEmpty()) { + remove(KEY_RETRY_IDS) + } else { + putStringSet(KEY_RETRY_IDS, ids.mapToSet { it.toString() }) + } + } + } + + private fun getRetryIds(): Set { + val prefs = applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE) + return prefs.getStringSet(KEY_RETRY_IDS, null)?.mapToSet { it.toLong() }.orEmpty() + } + private fun workDataOf(success: Int, failed: Int): Data { return Data.Builder() .putInt(DATA_KEY_SUCCESS, success) @@ -256,7 +295,7 @@ class TrackWorker @AssistedInject constructor( val request = PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) .setConstraints(constraints) .addTag(TAG) - .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.MINUTES) .build() workManager .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request) @@ -306,7 +345,9 @@ class TrackWorker @AssistedInject constructor( const val TAG = "tracking" const val TAG_ONESHOT = "tracking_oneshot" const val MAX_PARALLELISM = 3 + const val MAX_ATTEMPTS = 4 const val DATA_KEY_SUCCESS = "success" const val DATA_KEY_FAILED = "failed" + const val KEY_RETRY_IDS = "retry" } }