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> (cherry picked from commit cb639f4e90121b98221ba7358108200c46b6657f) # Conflicts: # app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt # app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
This commit is contained in:
parent
e1b834072c
commit
78e3330215
@ -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(), 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()) }
|
||||
|
@ -24,6 +24,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
|
||||
|
||||
@ -49,6 +50,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()
|
||||
@ -135,6 +139,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()
|
||||
}
|
||||
|
||||
@ -207,6 +219,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
|
||||
|
@ -5,8 +5,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
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
|
||||
@ -18,6 +17,7 @@ import java.util.Date
|
||||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
@ -87,16 +87,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
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
@ -136,6 +137,7 @@ fun MangaScreen(
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// SY -->
|
||||
onMetadataViewerClicked: () -> Unit,
|
||||
@ -175,6 +177,7 @@ fun MangaScreen(
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
@ -194,6 +197,7 @@ fun MangaScreen(
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
// SY -->
|
||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||
@ -222,6 +226,7 @@ fun MangaScreen(
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
@ -239,6 +244,7 @@ fun MangaScreen(
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
// SY -->
|
||||
onMetadataViewerClicked = onMetadataViewerClicked,
|
||||
@ -268,6 +274,7 @@ private fun MangaScreenSmallImpl(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
@ -294,6 +301,7 @@ private fun MangaScreenSmallImpl(
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// SY -->
|
||||
onMetadataViewerClicked: () -> Unit,
|
||||
@ -456,10 +464,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,
|
||||
// SY -->
|
||||
onMergeClicked = onMergeClicked.takeUnless { state.showMergeInOverflow },
|
||||
@ -570,6 +581,7 @@ fun MangaScreenLargeImpl(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
@ -596,6 +608,7 @@ fun MangaScreenLargeImpl(
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
// SY -->
|
||||
onMetadataViewerClicked: () -> Unit,
|
||||
@ -746,10 +759,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,
|
||||
// SY -->
|
||||
onMergeClicked = onMergeClicked.takeUnless { state.showMergeInOverflow },
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -12,15 +13,22 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
|
||||
@Composable
|
||||
fun DeleteChaptersDialog(
|
||||
@ -53,6 +61,54 @@ 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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@Composable
|
||||
fun SelectScanlatorsDialog(
|
||||
|
@ -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
|
||||
@ -163,17 +164,22 @@ 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)?,
|
||||
// SY -->
|
||||
onMergeClicked: (() -> Unit)?,
|
||||
// SY <--
|
||||
) {
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
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)) {
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
@ -185,6 +191,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) {
|
||||
|
@ -262,18 +262,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.BackupFlatMetadata
|
||||
@ -17,11 +18,16 @@ import exh.source.MERGED_SOURCE_ID
|
||||
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.CustomMangaInfo
|
||||
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
|
||||
|
||||
@ -29,6 +35,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)
|
||||
|
||||
@ -102,6 +114,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, sort by merged source so that merged source manga go last and merged references get the proper ids
|
||||
@ -153,7 +167,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/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
} else {
|
||||
@ -163,6 +177,8 @@ class BackupRestorer(
|
||||
// Fetch rest of manga information
|
||||
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
}
|
||||
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}")
|
||||
@ -195,10 +211,11 @@ class BackupRestorer(
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
customManga: CustomMangaInfo?,
|
||||
// SY <--
|
||||
) {
|
||||
): Manga {
|
||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||
backupManager.restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
@ -213,9 +230,10 @@ class BackupRestorer(
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
customManga: CustomMangaInfo?,
|
||||
// SY <--
|
||||
) {
|
||||
): Manga {
|
||||
backupManager.restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
|
||||
return backupManga
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(
|
||||
|
@ -45,7 +45,6 @@ import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.isMdBasedSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@ -80,12 +79,14 @@ 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.GetFavorites
|
||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.interactor.GetMergedMangaForDownloading
|
||||
import tachiyomi.domain.manga.interactor.InsertFlatMetadata
|
||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||
import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||
@ -95,6 +96,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
|
||||
@ -119,6 +121,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()
|
||||
|
||||
// SY -->
|
||||
private val getFavorites: GetFavorites = Injekt.get()
|
||||
@ -300,6 +303,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
|
||||
// SY -->
|
||||
val mdlistLogged = trackManager.services.any { it.isLogged && it.id == TrackManager.MDLIST }
|
||||
// SY <--
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
|
||||
val higherLimit = fetchRange.second
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }
|
||||
@ -310,6 +320,22 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
if (mdlistLogged && mangaInSource.firstOrNull()?.let { it.manga.source in mangaDexSourceIds } == true) {
|
||||
launch {
|
||||
mangaInSource.forEach { (manga) ->
|
||||
try {
|
||||
val tracks = getTracks.await(manga.id)
|
||||
if (tracks.isEmpty() || tracks.none { it.syncId == TrackManager.MDLIST }) {
|
||||
val track = trackManager.mdList.createInitialTracker(manga)
|
||||
insertTrack.await(trackManager.mdList.refresh(track).toDomainTrack(false)!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
xLogE("Error adding initial track for ${manga.title}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mangaInSource.forEach { libraryManga ->
|
||||
val manga = libraryManga.manga
|
||||
ensureActive()
|
||||
@ -325,6 +351,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))
|
||||
|
||||
@ -339,8 +368,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
else -> {
|
||||
try {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val newChapters = updateManga(manga, loggedServices)
|
||||
val newChapters = updateManga(manga, now, fetchRange)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
@ -428,7 +456,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, loggedServices: List<TrackService>): List<Chapter> = coroutineScope {
|
||||
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
|
||||
@ -437,34 +465,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
updateManga.awaitUpdateFromSource(manga, networkManga, manualFetch = false, coverCache)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
if (source.isMdBasedSource() && trackManager.mdList in loggedServices) {
|
||||
launch {
|
||||
try {
|
||||
val tracks = getTracks.await(manga.id)
|
||||
if (tracks.isEmpty() || tracks.none { it.syncId == TrackManager.MDLIST }) {
|
||||
val track = trackManager.mdList.createInitialTracker(manga)
|
||||
insertTrack.await(trackManager.mdList.refresh(track).toDomainTrack(false)!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
xLogE("Error adding initial track for ${manga.title}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source is MergedSource) {
|
||||
return@coroutineScope source.fetchChaptersAndSync(manga, false)
|
||||
return source.fetchChaptersAndSync(manga, false)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val chapters = source.getChapterList(manga.toSManga())
|
||||
|
||||
// Get manga from database to account for if it was removed during the update and
|
||||
// to get latest data so it doesn't get overwritten later on
|
||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return@coroutineScope emptyList()
|
||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||
|
||||
return@coroutineScope syncChaptersWithSource.await(chapters, dbManga, source)
|
||||
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
|
@ -34,6 +34,7 @@ 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.SelectScanlatorsDialog
|
||||
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
|
||||
@ -75,6 +76,7 @@ import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
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.domain.source.service.SourceManager
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
@ -139,6 +141,7 @@ class MangaScreen(
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
intervalDisplay = screenModel::intervalDisplay,
|
||||
isTabletUi = isTabletUi(),
|
||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||
@ -169,6 +172,7 @@ class MangaScreen(
|
||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
||||
onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in screenModel.libraryPreferences.libraryUpdateMangaRestriction().get() && successState.manga.favorite },
|
||||
// SY -->
|
||||
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||
@ -271,6 +275,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) },
|
||||
)
|
||||
}
|
||||
// SY -->
|
||||
is MangaScreenModel.Dialog.EditMangaInfo -> {
|
||||
EditMangaDialog(
|
||||
|
@ -111,6 +111,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.model.MergeMangaSettingsUpdate
|
||||
import tachiyomi.domain.manga.model.MergedMangaReference
|
||||
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.domain.track.interactor.InsertTrack
|
||||
@ -119,6 +120,7 @@ import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class MangaScreenModel(
|
||||
val context: Context,
|
||||
@ -126,9 +128,9 @@ class MangaScreenModel(
|
||||
private val isFromSource: Boolean,
|
||||
val smartSearched: 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(),
|
||||
@ -161,6 +163,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) {
|
||||
|
||||
@ -728,7 +731,7 @@ class MangaScreenModel(
|
||||
}
|
||||
|
||||
// Choose a category
|
||||
else -> promptChangeCategories()
|
||||
else -> showChangeCategoryDialog()
|
||||
}
|
||||
|
||||
// Finally match with enhanced tracking when available
|
||||
@ -754,7 +757,7 @@ class MangaScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun promptChangeCategories() {
|
||||
fun showChangeCategoryDialog() {
|
||||
val manga = successState?.manga ?: return
|
||||
coroutineScope.launch {
|
||||
val categories = getCategories()
|
||||
@ -770,6 +773,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.
|
||||
*/
|
||||
@ -977,6 +1013,7 @@ class MangaScreenModel(
|
||||
chapters,
|
||||
state.manga,
|
||||
state.source,
|
||||
manualFetch,
|
||||
)
|
||||
|
||||
if (manualFetch) {
|
||||
@ -997,6 +1034,8 @@ class MangaScreenModel(
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(message = message)
|
||||
}
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1475,6 +1514,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
|
||||
|
||||
// SY -->
|
||||
data class EditMangaInfo(val manga: Manga) : Dialog
|
||||
|
@ -13,26 +13,48 @@ import kotlin.math.absoluteValue
|
||||
|
||||
const val MAX_GRACE_PERIOD = 28
|
||||
|
||||
fun updateIntervalMeta(
|
||||
class SetMangaUpdateInterval(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
fun updateInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): MangaUpdate? {
|
||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
||||
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
setCurrentFetchRange
|
||||
fetchRange
|
||||
}
|
||||
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) }
|
||||
} else {
|
||||
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
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()
|
||||
}
|
||||
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)
|
||||
@ -53,7 +75,7 @@ fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): In
|
||||
}
|
||||
.distinct()
|
||||
|
||||
val newInterval = when {
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
@ -70,16 +92,17 @@ fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): In
|
||||
else -> 7
|
||||
}
|
||||
// Min 1, max 28 days
|
||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
currentFetchRange: Pair<Long, Long>,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
||||
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()
|
||||
@ -101,23 +124,4 @@ private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int
|
||||
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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user