More refactoring of expected next update logic

This commit is contained in:
arkon 2023-07-30 19:11:20 -04:00
parent c9a1bd86b5
commit 81cd765543
14 changed files with 101 additions and 278 deletions

View file

@ -50,13 +50,14 @@ class SyncChaptersWithSource(
manga: Manga,
source: Source,
manualFetch: Boolean = false,
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
fetchRange: Pair<Long, Long> = Pair(0, 0),
fetchWindow: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> {
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

View file

@ -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<Chapter>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
fetchRange: Pair<Long, Long> = setFetchInterval.getCurrent(zonedDateTime),
dateTime: ZonedDateTime = ZonedDateTime.now(),
window: Pair<Long, Long> = 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 {

View file

@ -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,

View file

@ -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))

View file

@ -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,

View file

@ -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))
}
},
)
}
}

View file

@ -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}")

View file

@ -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<Long, Long>): List<Chapter> {
private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
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() {

View file

@ -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) },
)

View file

@ -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<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedChapterIds: HashSet<Long> = 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,
)

View file

@ -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)

View file

@ -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<Chapter>,
zonedDateTime: ZonedDateTime,
fetchRange: Pair<Long, Long>,
dateTime: ZonedDateTime,
window: Pair<Long, Long>,
): 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<Long, Long> {
// 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<Long, Long> {
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<Chapter>, 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<Long, Long>,
dateTime: ZonedDateTime,
window: Pair<Long, Long>,
): 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
}

View file

@ -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<Chapter>()
(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<Chapter>()
(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<Chapter>()
(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<Chapter>()
(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)
}
}

View file

@ -259,17 +259,6 @@
<string name="pref_library_update_show_tab_badge">Show unread count on Updates icon</string>
<string name="pref_update_only_in_release_period">Outside expected release period</string>
<string name="pref_update_release_grace_period">Expected release grace period</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d day before</item>
<item quantity="other">%d days before</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d day after</item>
<item quantity="other">%d days after</item>
</plurals>
<string name="pref_update_release_grace_period_info">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.</string>
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
@ -637,10 +626,6 @@
<item quantity="one">1 day</item>
<item quantity="other">%d days</item>
</plurals>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d day</item>
<item quantity="other">%1$d - %2$d days</item>
</plurals>
<!-- Manga info -->
<plurals name="missing_chapters">