From 81cd765543bbb5901e11a12921adaea99c51810c Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 30 Jul 2023 19:11:20 -0400 Subject: [PATCH] More refactoring of expected next update logic --- .../interactor/SyncChaptersWithSource.kt | 15 +-- .../domain/manga/interactor/UpdateManga.kt | 15 +-- .../kanade/presentation/manga/MangaScreen.kt | 7 +- .../manga/components/MangaDialogs.kt | 12 +- .../manga/components/MangaInfoHeader.kt | 12 +- .../settings/screen/SettingsLibraryScreen.kt | 127 +----------------- .../tachiyomi/data/backup/BackupRestorer.kt | 11 +- .../data/library/LibraryUpdateJob.kt | 25 ++-- .../kanade/tachiyomi/ui/manga/MangaScreen.kt | 11 +- .../tachiyomi/ui/manga/MangaScreenModel.kt | 25 +--- .../library/service/LibraryPreferences.kt | 3 - .../manga/interactor/SetFetchInterval.kt | 78 +++++------ .../manga/interactor/SetFetchIntervalTest.kt | 23 ++-- i18n/src/main/res/values/strings.xml | 15 --- 14 files changed, 101 insertions(+), 278 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt index f7270e6610..225f0cb424 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt @@ -50,13 +50,14 @@ class SyncChaptersWithSource( manga: Manga, source: Source, manualFetch: Boolean = false, - zoneDateTime: ZonedDateTime = ZonedDateTime.now(), - fetchRange: Pair = Pair(0, 0), + fetchWindow: Pair = Pair(0, 0), ): List { if (rawSourceChapters.isEmpty() && !source.isLocal()) { throw NoChaptersException() } + val now = ZonedDateTime.now() + val sourceChapters = rawSourceChapters .distinctBy { it.url } .mapIndexed { i, sChapter -> @@ -138,12 +139,11 @@ class SyncChaptersWithSource( // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { - if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchRange.first) { + if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { updateManga.awaitUpdateFetchInterval( manga, - dbChapters, - zoneDateTime, - fetchRange, + now, + fetchWindow, ) } return emptyList() @@ -200,8 +200,7 @@ class SyncChaptersWithSource( val chapterUpdates = toChange.map { it.toChapterUpdate() } updateChapter.awaitAll(chapterUpdates) } - val newChapters = chapterRepository.getChapterByMangaId(manga.id) - updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange) + updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) // Set this manga as updated since chapters were changed // Note that last_update actually represents last time the chapter list changed at all diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index f17214dbed..38d6083ff8 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -3,7 +3,6 @@ package eu.kanade.domain.manga.interactor import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.SManga -import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.interactor.SetFetchInterval import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate @@ -79,16 +78,12 @@ class UpdateManga( suspend fun awaitUpdateFetchInterval( manga: Manga, - chapters: List, - zonedDateTime: ZonedDateTime = ZonedDateTime.now(), - fetchRange: Pair = setFetchInterval.getCurrent(zonedDateTime), + dateTime: ZonedDateTime = ZonedDateTime.now(), + window: Pair = setFetchInterval.getWindow(dateTime), ): Boolean { - val updatedManga = setFetchInterval.update(manga, chapters, zonedDateTime, fetchRange) - return if (updatedManga != null) { - mangaRepository.update(updatedManga) - } else { - true - } + return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window) + ?.let { mangaRepository.update(it) } + ?: false } suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index d52c540ce5..740ece4a26 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -62,7 +62,6 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.source.getNameForMangaInfo import eu.kanade.tachiyomi.ui.manga.ChapterItem -import eu.kanade.tachiyomi.ui.manga.FetchInterval import eu.kanade.tachiyomi.ui.manga.MangaScreenModel import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -85,7 +84,7 @@ import java.util.Date fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - fetchInterval: FetchInterval?, + fetchInterval: Int?, dateFormat: DateFormat, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, @@ -217,7 +216,7 @@ private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, dateFormat: DateFormat, - fetchInterval: FetchInterval?, + fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, @@ -448,7 +447,7 @@ fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, dateFormat: DateFormat, - fetchInterval: FetchInterval?, + fetchInterval: Int?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index fac4dfd370..84d9692472 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD +import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL import tachiyomi.presentation.core.components.WheelTextPicker @Composable @@ -56,7 +56,7 @@ fun SetIntervalDialog( onDismissRequest: () -> Unit, onValueChanged: (Int) -> Unit, ) { - var intervalValue by rememberSaveable { mutableIntStateOf(interval) } + var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } AlertDialog( onDismissRequest = onDismissRequest, @@ -67,7 +67,7 @@ fun SetIntervalDialog( contentAlignment = Alignment.Center, ) { val size = DpSize(width = maxWidth / 2, height = 128.dp) - val items = (0..MAX_GRACE_PERIOD).map { + val items = (0..MAX_FETCH_INTERVAL).map { if (it == 0) { stringResource(R.string.label_default) } else { @@ -77,8 +77,8 @@ fun SetIntervalDialog( WheelTextPicker( size = size, items = items, - startIndex = intervalValue, - onSelectionChanged = { intervalValue = it }, + startIndex = selectedInterval, + onSelectionChanged = { selectedInterval = it }, ) } }, @@ -89,7 +89,7 @@ fun SetIntervalDialog( }, confirmButton = { TextButton(onClick = { - onValueChanged(intervalValue) + onValueChanged(selectedInterval) onDismissRequest() },) { Text(text = stringResource(R.string.action_ok)) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 821dda5f0f..5e0efe42bf 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -78,13 +78,13 @@ import coil.compose.AsyncImage import eu.kanade.presentation.components.DropdownMenu import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.ui.manga.FetchInterval import eu.kanade.tachiyomi.util.system.copyToClipboard import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.secondaryItemAlpha +import kotlin.math.absoluteValue import kotlin.math.roundToInt private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) @@ -166,7 +166,7 @@ fun MangaActionRow( modifier: Modifier = Modifier, favorite: Boolean, trackingCount: Int, - fetchInterval: FetchInterval?, + fetchInterval: Int?, isUserIntervalMode: Boolean, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, @@ -190,14 +190,8 @@ fun MangaActionRow( onLongClick = onEditCategory, ) if (onEditIntervalClicked != null && fetchInterval != null) { - val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays) MangaActionButton( - title = - if (intervalPair.first == intervalPair.second) { - pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second) - } else { - pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second) - }, + title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue), icon = Icons.Default.HourglassEmpty, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, onClick = onEditIntervalClicked, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 1af6636df6..5ae741135e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -1,33 +1,18 @@ package eu.kanade.presentation.more.settings.screen import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.core.content.ContextCompat import cafe.adriel.voyager.navigator.LocalNavigator @@ -54,8 +39,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD -import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD -import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -141,13 +124,10 @@ object SettingsLibraryScreen : SearchableSettings { val context = LocalContext.current val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval() - val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction() - val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateMangaRestriction() val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories() val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude() val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState() - val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState() val included by libraryUpdateCategoriesPref.collectAsState() val excluded by libraryUpdateCategoriesExcludePref.collectAsState() @@ -168,25 +148,10 @@ object SettingsLibraryScreen : SearchableSettings { }, ) } - val leadRange by libraryPreferences.leadingExpectedDays().collectAsState() - val followRange by libraryPreferences.followingExpectedDays().collectAsState() - var showFetchRangesDialog by rememberSaveable { mutableStateOf(false) } - if (showFetchRangesDialog) { - LibraryExpectedRangeDialog( - initialLead = leadRange, - initialFollow = followRange, - onDismissRequest = { showFetchRangesDialog = false }, - onValueChanged = { leadValue, followValue -> - libraryPreferences.leadingExpectedDays().set(leadValue) - libraryPreferences.followingExpectedDays().set(followValue) - showFetchRangesDialog = false - }, - ) - } return Preference.PreferenceGroup( title = stringResource(R.string.pref_category_library_update), - preferenceItems = listOfNotNull( + preferenceItems = listOf( Preference.PreferenceItem.ListPreference( pref = libraryUpdateIntervalPref, title = stringResource(R.string.pref_library_update_interval), @@ -204,7 +169,7 @@ object SettingsLibraryScreen : SearchableSettings { }, ), Preference.PreferenceItem.MultiSelectListPreference( - pref = libraryUpdateDeviceRestrictionPref, + pref = libraryPreferences.libraryUpdateDeviceRestriction(), enabled = libraryUpdateInterval > 0, title = stringResource(R.string.pref_library_update_restriction), subtitle = stringResource(R.string.restrictions), @@ -241,7 +206,7 @@ object SettingsLibraryScreen : SearchableSettings { subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary), ), Preference.PreferenceItem.MultiSelectListPreference( - pref = libraryUpdateMangaRestrictionPref, + pref = libraryPreferences.libraryUpdateMangaRestriction(), title = stringResource(R.string.pref_library_update_manga_restriction), entries = mapOf( MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read), @@ -250,17 +215,6 @@ object SettingsLibraryScreen : SearchableSettings { MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period), ), ), - Preference.PreferenceItem.TextPreference( - title = stringResource(R.string.pref_update_release_grace_period), - subtitle = listOf( - pluralStringResource(R.plurals.pref_update_release_leading_days, leadRange, leadRange), - pluralStringResource(R.plurals.pref_update_release_following_days, followRange, followRange), - ).joinToString(), - onClick = { showFetchRangesDialog = true }, - ).takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction }, - Preference.PreferenceItem.InfoPreference( - title = stringResource(R.string.pref_update_release_grace_period_info), - ).takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction }, Preference.PreferenceItem.SwitchPreference( pref = libraryPreferences.newShowUpdatesCount(), title = stringResource(R.string.pref_library_update_show_tab_badge), @@ -299,79 +253,4 @@ object SettingsLibraryScreen : SearchableSettings { ), ) } - - @Composable - private fun LibraryExpectedRangeDialog( - initialLead: Int, - initialFollow: Int, - onDismissRequest: () -> Unit, - onValueChanged: (portrait: Int, landscape: Int) -> Unit, - ) { - var leadValue by rememberSaveable { mutableIntStateOf(initialLead) } - var followValue by rememberSaveable { mutableIntStateOf(initialFollow) } - - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) }, - text = { - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.weight(1f), - text = pluralStringResource(R.plurals.pref_update_release_leading_days, leadValue, leadValue), - textAlign = TextAlign.Center, - maxLines = 1, - style = MaterialTheme.typography.labelMedium, - ) - Text( - modifier = Modifier.weight(1f), - text = pluralStringResource(R.plurals.pref_update_release_following_days, followValue, followValue), - textAlign = TextAlign.Center, - maxLines = 1, - style = MaterialTheme.typography.labelMedium, - ) - } - } - BoxWithConstraints( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - val size = DpSize(width = maxWidth / 2, height = 128.dp) - val items = (0..MAX_GRACE_PERIOD).map(Int::toString) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - WheelTextPicker( - size = size, - items = items, - startIndex = leadValue, - onSelectionChanged = { - leadValue = it - }, - ) - WheelTextPicker( - size = size, - items = items, - startIndex = followValue, - onSelectionChanged = { - followValue = it - }, - ) - } - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(android.R.string.cancel)) - } - }, - confirmButton = { - TextButton(onClick = { onValueChanged(leadValue, followValue) }) { - Text(text = stringResource(R.string.action_ok)) - } - }, - ) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 773231cced..544c5c18d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -33,8 +33,8 @@ class BackupRestorer( private val chapterRepository: ChapterRepository = Injekt.get() private val setFetchInterval: SetFetchInterval = Injekt.get() - private var zonedDateTime = ZonedDateTime.now() - private var currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime) + private var now = ZonedDateTime.now() + private var currentFetchWindow = setFetchInterval.getWindow(now) private var backupManager = BackupManager(context) @@ -102,8 +102,8 @@ class BackupRestorer( // Store source mapping for error messages val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources sourceMapping = backupMaps.associate { it.sourceId to it.name } - zonedDateTime = ZonedDateTime.now() - currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime) + now = ZonedDateTime.now() + currentFetchWindow = setFetchInterval.getWindow(now) return coroutineScope { // Restore individual manga @@ -146,8 +146,7 @@ class BackupRestorer( // Fetch rest of manga information restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories) } - val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id) - updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentFetchInterval) + updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow) } catch (e: Exception) { val sourceName = sourceMapping[manga.source] ?: manga.source.toString() errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 28ebb22e53..f4cc2682b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -231,9 +231,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val hasDownloads = AtomicBoolean(false) val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get() - val now = ZonedDateTime.now() - val fetchInterval = setFetchInterval.getCurrent(now) - val higherLimit = fetchInterval.second + val fetchWindow by lazy { setFetchInterval.getWindow(ZonedDateTime.now()) } coroutineScope { mangaToUpdate.groupBy { it.manga.source }.values @@ -255,8 +253,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet manga, ) { when { - MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period)) + manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> + skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update)) MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED -> skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed)) @@ -267,12 +265,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted -> skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started)) - manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> - skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update)) + MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate !in fetchWindow.first.rangeTo(fetchWindow.second) -> + skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period)) else -> { try { - val newChapters = updateManga(manga, now, fetchInterval) + val newChapters = updateManga(manga, fetchWindow) .sortedByDescending { it.sourceOrder } if (newChapters.isNotEmpty()) { @@ -328,6 +326,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet ) } if (skippedUpdates.isNotEmpty()) { + // TODO: surface skipped reasons to user + logcat { + skippedUpdates + .groupBy { it.second } + .map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" } + .joinToString() + } notifier.showUpdateSkippedNotification(skippedUpdates.size) } } @@ -344,7 +349,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet * @param manga the manga to update. * @return a pair of the inserted and removed chapters. */ - private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair): List { + private suspend fun updateManga(manga: Manga, fetchWindow: Pair): List { val source = sourceManager.getOrStub(manga.source) // Update manga metadata if needed @@ -359,7 +364,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // to get latest data so it doesn't get overwritten later on val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList() - return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange) + return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow) } private suspend fun updateCovers() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index 58a1755329..8f56c4fcb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -83,13 +83,6 @@ class MangaScreen( val successState = state as MangaScreenModel.State.Success val isHttpSource = remember { successState.source is HttpSource } - val fetchInterval = remember(successState.manga.fetchInterval) { - FetchInterval( - interval = successState.manga.fetchInterval, - leadDays = screenModel.leadDay, - followDays = screenModel.followDay, - ) - } LaunchedEffect(successState.manga, screenModel.source) { if (isHttpSource) { @@ -107,7 +100,7 @@ class MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, dateFormat = screenModel.dateFormat, - fetchInterval = fetchInterval, + fetchInterval = successState.manga.fetchInterval, isTabletUi = isTabletUi(), chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeEndAction = screenModel.chapterSwipeEndAction, @@ -218,7 +211,7 @@ class MangaScreen( } is MangaScreenModel.Dialog.SetFetchInterval -> { SetIntervalDialog( - interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0, + interval = dialog.manga.fetchInterval, onDismissRequest = onDismissRequest, onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt index ddc1c6f79c..da1cd4ccdd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt @@ -129,8 +129,6 @@ class MangaScreenModel( private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get() - val leadDay = libraryPreferences.leadingExpectedDays().get() - val followDay = libraryPreferences.followingExpectedDays().get() private val selectedPositions: Array = arrayOf(-1, -1) // first and last selected index in list private val selectedChapterIds: HashSet = HashSet() @@ -361,20 +359,14 @@ class MangaScreenModel( } } - fun setFetchInterval(manga: Manga, newInterval: Int) { - val interval = when (newInterval) { - // reset interval 0 default to trigger recalculation - // only reset if interval is custom, which is negative - 0 -> if (manga.fetchInterval < 0) 0 else manga.fetchInterval - else -> -newInterval - } + fun setFetchInterval(manga: Manga, interval: Int) { coroutineScope.launchIO { updateManga.awaitUpdateFetchInterval( - manga.copy(fetchInterval = interval), - successState?.chapters?.map { it.chapter }.orEmpty(), + // Custom intervals are negative + manga.copy(fetchInterval = -interval), ) - val newManga = mangaRepository.getMangaById(mangaId) - updateSuccessState { it.copy(manga = newManga) } + val updatedManga = mangaRepository.getMangaById(manga.id) + updateSuccessState { it.copy(manga = updatedManga) } } } @@ -1055,10 +1047,3 @@ data class ChapterItem( ) { val isDownloaded = downloadState == Download.State.DOWNLOADED } - -@Immutable -data class FetchInterval( - val interval: Int, - val leadDays: Int, - val followDays: Int, -) diff --git a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt index efec8755b4..b120dcaf69 100644 --- a/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt @@ -38,9 +38,6 @@ class LibraryPreferences( ), ) - fun leadingExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1) - fun followingExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1) - fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false) diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt index ca46e58377..35c6bb22e4 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt @@ -1,35 +1,34 @@ package tachiyomi.domain.manga.interactor +import tachiyomi.domain.chapter.interactor.GetChapterByMangaId import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaUpdate -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import java.time.Instant import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import kotlin.math.absoluteValue -const val MAX_GRACE_PERIOD = 28 +const val MAX_FETCH_INTERVAL = 28 +private const val FETCH_INTERVAL_GRACE_PERIOD = 1 class SetFetchInterval( - private val libraryPreferences: LibraryPreferences = Injekt.get(), + private val getChapterByMangaId: GetChapterByMangaId, ) { - fun update( + suspend fun toMangaUpdateOrNull( manga: Manga, - chapters: List, - zonedDateTime: ZonedDateTime, - fetchRange: Pair, + dateTime: ZonedDateTime, + window: Pair, ): MangaUpdate? { - val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) { - getCurrent(ZonedDateTime.now()) + val currentWindow = if (window.first == 0L && window.second == 0L) { + getWindow(ZonedDateTime.now()) } else { - fetchRange + window } - val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime) - val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval) + val chapters = getChapterByMangaId.await(manga.id) + val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime) + val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow) return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) { null @@ -38,20 +37,11 @@ class SetFetchInterval( } } - fun getCurrent(timeToCal: ZonedDateTime): Pair { - // lead range and the following range depend on if updateOnlyExpectedPeriod set. - var followRange = 0 - var leadRange = 0 - if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()) { - followRange = libraryPreferences.followingExpectedDays().get() - leadRange = libraryPreferences.leadingExpectedDays().get() - } - val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone) - // revert math of (next_update + follow < now) become (next_update < now - follow) - // so (now - follow) become lower limit - val lowerRange = startToday.minusDays(followRange.toLong()) - val higherRange = startToday.plusDays(leadRange.toLong()) - return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1) + fun getWindow(dateTime: ZonedDateTime): Pair { + val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone) + val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong()) + val upperBound = lowerBound.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong()) + return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1) } internal fun calculateInterval(chapters: List, zonedDateTime: ZonedDateTime): Int { @@ -91,35 +81,41 @@ class SetFetchInterval( // Default to 7 days else -> 7 } - // Min 1, max 28 days - return interval.coerceIn(1, MAX_GRACE_PERIOD) + + return interval.coerceIn(1, MAX_FETCH_INTERVAL) } private fun calculateNextUpdate( manga: Manga, interval: Int, - zonedDateTime: ZonedDateTime, - fetchRange: Pair, + dateTime: ZonedDateTime, + window: Pair, ): Long { return if ( - manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) || + manga.nextUpdate !in window.first.rangeTo(window.second + 1) || manga.fetchInterval == 0 ) { - val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay() - val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt() - val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) - latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 + val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone) + .toLocalDate() + .atStartOfDay() + val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt() + val cycle = timeSinceLatest.floorDiv( + interval.absoluteValue.takeIf { interval < 0 } + ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10), + ) + latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000 } else { manga.nextUpdate } } - private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int { - if (delta >= maxValue) return maxValue - val cycle = timeSinceLatest.floorDiv(delta) + 1 + private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int { + if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL + // double delta again if missed more than 9 check in new delta + val cycle = timeSinceLatest.floorDiv(delta) + 1 return if (cycle > doubleWhenOver) { - doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue) + doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver) } else { delta } diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt index d6eadbfab6..d497d0b051 100644 --- a/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt +++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt @@ -11,6 +11,7 @@ import java.time.ZonedDateTime @Execution(ExecutionMode.CONCURRENT) class SetFetchIntervalTest { + private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") private var chapter = Chapter.create().copy( dateFetch = testTime.toEpochSecond() * 1000, @@ -19,14 +20,8 @@ class SetFetchIntervalTest { private val setFetchInterval = SetFetchInterval(mockk()) - private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter { - val newTime = testTime.plus(duration).toEpochSecond() * 1000 - return chapter.copy(dateFetch = newTime, dateUpload = newTime) - } - - // default 7 when less than 3 distinct day @Test - fun `calculateInterval returns 7 when 1 chapters in 1 day`() { + fun `calculateInterval returns default of 7 days when less than 3 distinct days`() { val chapters = mutableListOf() (1..1).forEach { val duration = Duration.ofHours(10) @@ -63,9 +58,8 @@ class SetFetchIntervalTest { setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7 } - // Default 1 if interval less than 1 @Test - fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() { + fun `calculateInterval returns default of 1 day when interval less than 1`() { val chapters = mutableListOf() (1..5).forEach { val duration = Duration.ofHours(15L * it) @@ -98,9 +92,8 @@ class SetFetchIntervalTest { setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2 } - // If interval is decimal, floor to closest integer @Test - fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() { + fun `calculateInterval returns floored value when interval is decimal`() { val chapters = mutableListOf() (1..5).forEach { val duration = Duration.ofHours(25L * it) @@ -121,9 +114,8 @@ class SetFetchIntervalTest { setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 } - // Use fetch time if upload time not available @Test - fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() { + fun `calculateInterval returns interval based on fetch time if upload time not available`() { val chapters = mutableListOf() (1..5).forEach { val duration = Duration.ofHours(25L * it) @@ -132,4 +124,9 @@ class SetFetchIntervalTest { } setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 } + + private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter { + val newTime = testTime.plus(duration).toEpochSecond() * 1000 + return chapter.copy(dateFetch = newTime, dateUpload = newTime) + } } diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index cab6803c60..596de0f941 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -259,17 +259,6 @@ Show unread count on Updates icon Outside expected release period - Expected release grace period - - %d day before - %d days before - - - %d day after - %d days after - - A low grace period is recommended to minimize stress on sources. The more checks for an entry that are missed, the longer the interval in between checks will be with a maximum of 28 days. - Automatically refresh metadata Check for new cover and details when updating library Automatically refresh trackers @@ -637,10 +626,6 @@ 1 day %d days - - %1$d - %2$d day - %1$d - %2$d days -