Update Manga in Expected Period (#5734)
* Add Predict Interval Test * Get mangas next update and interval in library update * Get next update and interval in backup restore * Display and set intervals, nextUpdate in Manga Info * Move logic function to MangeScreen and InfoHeader Update per suggestion --------- Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
parent
6d69caf59e
commit
cb639f4e90
14 changed files with 460 additions and 131 deletions
|
@ -57,6 +57,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
|||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||
import tachiyomi.domain.release.service.ReleaseService
|
||||
|
@ -100,10 +101,11 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { SetMangaUpdateInterval(get()) }
|
||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get()) }
|
||||
addFactory { UpdateManga(get(), get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.isLocal
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Long.max
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -48,6 +49,9 @@ class SyncChaptersWithSource(
|
|||
rawSourceChapters: List<SChapter>,
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
manualFetch: Boolean = false,
|
||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
|
@ -134,6 +138,14 @@ 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.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
dbChapters,
|
||||
zoneDateTime,
|
||||
fetchRange,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
|
@ -188,6 +200,8 @@ class SyncChaptersWithSource(
|
|||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
|
|
|
@ -4,8 +4,7 @@ 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.getCurrentFetchRange
|
||||
import tachiyomi.domain.manga.interactor.updateIntervalMeta
|
||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
|||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
|
@ -77,16 +77,15 @@ class UpdateManga(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateIntervalMeta(
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||
): Boolean {
|
||||
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
|
||||
|
||||
return if (newMeta != null) {
|
||||
mangaRepository.update(newMeta)
|
||||
val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
|
||||
return if (updatedManga != null) {
|
||||
mangaRepository.update(updatedManga)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ fun MangaScreen(
|
|||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -112,6 +113,7 @@ fun MangaScreen(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
|
@ -141,6 +143,7 @@ fun MangaScreen(
|
|||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
|
@ -160,6 +163,7 @@ fun MangaScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
|
@ -178,6 +182,7 @@ fun MangaScreen(
|
|||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
|
@ -195,6 +200,7 @@ fun MangaScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
|
@ -214,6 +220,7 @@ private fun MangaScreenSmallImpl(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -240,6 +247,7 @@ private fun MangaScreenSmallImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
|
@ -383,10 +391,13 @@ private fun MangaScreenSmallImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
}
|
||||
|
@ -440,6 +451,7 @@ fun MangaScreenLargeImpl(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -466,6 +478,7 @@ fun MangaScreenLargeImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
|
@ -596,10 +609,13 @@ fun MangaScreenLargeImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
ExpandableMangaDescription(
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.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.presentation.core.components.WheelTextPicker
|
||||
|
||||
@Composable
|
||||
fun DeleteChaptersDialog(
|
||||
|
@ -37,3 +49,51 @@ fun DeleteChaptersDialog(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetIntervalDialog(
|
||||
interval: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
) {
|
||||
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
|
||||
text = {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val items = (0..MAX_GRACE_PERIOD).map {
|
||||
if (it == 0) {
|
||||
stringResource(R.string.label_default)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
WheelTextPicker(
|
||||
size = size,
|
||||
items = items,
|
||||
startIndex = intervalValue,
|
||||
onSelectionChanged = { intervalValue = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(intervalValue)
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
|
@ -164,14 +165,19 @@ fun MangaActionRow(
|
|||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onWebViewLongClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
) {
|
||||
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
|
@ -183,6 +189,19 @@ fun MangaActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && interval != null) {
|
||||
MangaActionButton(
|
||||
title =
|
||||
if (interval.first == interval.second) {
|
||||
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
|
|
|
@ -241,18 +241,15 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
title = stringResource(R.string.pref_library_update_refresh_trackers),
|
||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||
),
|
||||
// TODO: remove isDevFlavor checks once functionality is available
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateMangaRestrictionPref,
|
||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||
entries = buildMap {
|
||||
put(MANGA_HAS_UNREAD, stringResource(R.string.pref_update_only_completely_read))
|
||||
put(MANGA_NON_READ, stringResource(R.string.pref_update_only_started))
|
||||
put(MANGA_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
|
||||
if (isDevFlavor) {
|
||||
put(MANGA_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
|
||||
}
|
||||
},
|
||||
entries = mapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||
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),
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
|
@ -12,10 +13,15 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -23,6 +29,12 @@ class BackupRestorer(
|
|||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
) {
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||
|
||||
private var zonedDateTime = ZonedDateTime.now()
|
||||
private var currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
|
@ -90,6 +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()
|
||||
currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
|
||||
return coroutineScope {
|
||||
// Restore individual manga
|
||||
|
@ -122,7 +136,7 @@ class BackupRestorer(
|
|||
|
||||
try {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||
if (dbManga == null) {
|
||||
val restoredManga = if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
|
@ -132,6 +146,8 @@ 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, currentRange)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
|
@ -159,10 +175,11 @@ class BackupRestorer(
|
|||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Manga {
|
||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||
backupManager.restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
|
@ -172,9 +189,10 @@ class BackupRestorer(
|
|||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Manga {
|
||||
backupManager.restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||
return backupManga
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||
|
|
|
@ -66,8 +66,10 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
|||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
|
||||
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.GetLibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||
import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -101,6 +104,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
private val getTracks: GetTracks = Injekt.get()
|
||||
private val insertTrack: InsertTrack = Injekt.get()
|
||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||
|
||||
private val notifier = LibraryUpdateNotifier(context)
|
||||
|
||||
|
@ -227,6 +231,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
|
||||
val higherLimit = fetchRange.second
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }.values
|
||||
.map { mangaInSource ->
|
||||
|
@ -247,6 +255,9 @@ 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_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
||||
|
||||
|
@ -261,7 +272,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
|
||||
else -> {
|
||||
try {
|
||||
val newChapters = updateManga(manga)
|
||||
val newChapters = updateManga(manga, now, fetchRange)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
|
@ -333,7 +344,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): List<Chapter> {
|
||||
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
|
||||
// Update manga metadata if needed
|
||||
|
@ -348,7 +359,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)
|
||||
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
|
|
|
@ -30,6 +30,7 @@ import eu.kanade.presentation.manga.EditCoverAction
|
|||
import eu.kanade.presentation.manga.MangaScreen
|
||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
|
||||
import eu.kanade.presentation.manga.components.MangaCoverDialog
|
||||
import eu.kanade.presentation.manga.components.SetIntervalDialog
|
||||
import eu.kanade.presentation.util.AssistContentScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
|
@ -53,6 +54,7 @@ import logcat.LogPriority
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
|
@ -100,6 +102,7 @@ class MangaScreen(
|
|||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
intervalDisplay = screenModel::intervalDisplay,
|
||||
isTabletUi = isTabletUi(),
|
||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||
|
@ -121,7 +124,8 @@ class MangaScreen(
|
|||
onCoverClicked = screenModel::showCoverDialog,
|
||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||
onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in screenModel.libraryPreferences.libraryUpdateMangaRestriction().get() && successState.manga.favorite },
|
||||
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
||||
|
@ -207,6 +211,13 @@ class MangaScreen(
|
|||
LoadingScreen(Modifier.systemBarsPadding())
|
||||
}
|
||||
}
|
||||
is MangaScreenModel.Dialog.SetMangaInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = if (dialog.manga.calculateInterval < 0) -dialog.manga.calculateInterval else 0,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onValueChanged = { screenModel.setFetchRangeInterval(dialog.manga, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,20 +69,22 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
|||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.applyFilter
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class MangaScreenModel(
|
||||
val context: Context,
|
||||
val mangaId: Long,
|
||||
private val isFromSource: Boolean,
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
val readerPreferences: ReaderPreferences = Injekt.get(),
|
||||
val uiPreferences: UiPreferences = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadCache: DownloadCache = Injekt.get(),
|
||||
|
@ -97,6 +99,7 @@ class MangaScreenModel(
|
|||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
||||
|
||||
|
@ -307,7 +310,7 @@ class MangaScreenModel(
|
|||
}
|
||||
|
||||
// Choose a category
|
||||
else -> promptChangeCategories()
|
||||
else -> showChangeCategoryDialog()
|
||||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
|
@ -333,7 +336,7 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun promptChangeCategories() {
|
||||
fun showChangeCategoryDialog() {
|
||||
val manga = successState?.manga ?: return
|
||||
coroutineScope.launch {
|
||||
val categories = getCategories()
|
||||
|
@ -349,6 +352,39 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun showSetMangaIntervalDialog() {
|
||||
val manga = successState?.manga ?: return
|
||||
updateSuccessState {
|
||||
it.copy(dialog = Dialog.SetMangaInterval(manga))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should be in the state/composables
|
||||
fun intervalDisplay(): Pair<Int, Int>? {
|
||||
val state = successState ?: return null
|
||||
val leadDay = libraryPreferences.leadingExpectedDays().get()
|
||||
val followDay = libraryPreferences.followingExpectedDays().get()
|
||||
val effInterval = state.manga.calculateInterval
|
||||
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||
}
|
||||
|
||||
fun setFetchRangeInterval(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.calculateInterval < 0) 0 else manga.calculateInterval
|
||||
else -> -newInterval
|
||||
}
|
||||
coroutineScope.launchIO {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga.copy(calculateInterval = interval),
|
||||
successState?.chapters?.map { it.chapter }.orEmpty(),
|
||||
)
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
|
@ -502,6 +538,7 @@ class MangaScreenModel(
|
|||
chapters,
|
||||
state.manga,
|
||||
state.source,
|
||||
manualFetch,
|
||||
)
|
||||
|
||||
if (manualFetch) {
|
||||
|
@ -519,6 +556,8 @@ class MangaScreenModel(
|
|||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(message = message)
|
||||
}
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -943,6 +982,7 @@ class MangaScreenModel(
|
|||
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||
data class SetMangaInterval(val manga: Manga) : Dialog
|
||||
data object SettingsSheet : Dialog
|
||||
data object TrackSheet : Dialog
|
||||
data object FullCover : Dialog
|
||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
|||
|
||||
const val MAX_GRACE_PERIOD = 28
|
||||
|
||||
fun updateIntervalMeta(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
): MangaUpdate? {
|
||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
setCurrentFetchRange
|
||||
}
|
||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||
class SetMangaUpdateInterval(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||
null
|
||||
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
||||
}
|
||||
|
||||
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedChapters = chapters
|
||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedChapters
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
fun updateInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): MangaUpdate? {
|
||||
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
fetchRange
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||
|
||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||
null
|
||||
} else {
|
||||
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
val newInterval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||
fun getCurrentFetchRange(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()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||
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)
|
||||
}
|
||||
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedChapters = chapters
|
||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedChapters
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
// Min 1, max 28 days
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
// Min 1, max 28 days
|
||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
currentFetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
||||
manga.calculateInterval == 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
|
||||
} else {
|
||||
manga.nextUpdate
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||
manga.calculateInterval == 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
|
||||
} 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
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentFetchRange(
|
||||
timeToCal: ZonedDateTime,
|
||||
): Pair<Long, Long> {
|
||||
val preferences: LibraryPreferences = Injekt.get()
|
||||
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateMangaRestriction().get()) {
|
||||
followRange = preferences.followingExpectedDays().get()
|
||||
leadRange = preferences.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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package tachiyomi.domain.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetMangaUpdateIntervalTest {
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val setMangaUpdateInterval = SetMangaUpdateInterval(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`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..2).forEach {
|
||||
val duration = Duration.ofHours(24L)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.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`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Normal interval calculation
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(24L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.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`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(43L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.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`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
}
|
|
@ -641,6 +641,10 @@
|
|||
<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">
|
||||
|
@ -682,8 +686,7 @@
|
|||
<string name="display_mode_chapter">Chapter %1$s</string>
|
||||
<string name="manga_display_interval_title">Estimate every</string>
|
||||
<string name="manga_display_modified_interval_title">Set to update every</string>
|
||||
<string name="manga_modify_interval_title">Modify interval</string>
|
||||
<string name="manga_modify_calculated_interval_title">Customize Interval</string>
|
||||
<string name="manga_modify_calculated_interval_title">Customize interval</string>
|
||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||
<string name="chapter_error">Error</string>
|
||||
<string name="chapter_paused">Paused</string>
|
||||
|
|
Reference in a new issue