Replace java.time APIs with kotlin(x).(date)time (#3001)

* Replace java.time APIs with kotlin(x).(date)time

kotlinx-datetime explicitly says in its README that it does not cover
i18n, so we have to stick with using their toJavaX converters wherever
we localise dates and times.

Yes, some of these replacements are... questionable. But I wanted to
try and maximise the replacement for now.

* Bump kotlinx-datetime to 0.8.0

* More kotlin.time/kotlinx-datetime replacements

* Remove redundant init block & make attributes val

* Replace new use of java.time.Instant
This commit is contained in:
MajorTanya
2026-08-02 12:58:25 +02:00
committed by GitHub
parent 0648e2eaaf
commit 6d69903a56
55 changed files with 365 additions and 268 deletions
@@ -1,14 +1,20 @@
package tachiyomi.domain.manga.interactor
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.daysUntil
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Instant
class FetchInterval(
private val getChaptersByMangaId: GetChaptersByMangaId,
@@ -16,40 +22,42 @@ class FetchInterval(
suspend fun toMangaUpdate(
manga: Manga,
dateTime: ZonedDateTime,
dateTime: LocalDateTime,
timeZone: TimeZone,
window: Pair<Long, Long>,
): MangaUpdate {
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
zone = dateTime.zone,
zone = timeZone,
)
val currentWindow = if (window.first == 0L && window.second == 0L) {
getWindow(ZonedDateTime.now())
getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
} else {
window
}
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow)
return MangaUpdate(id = manga.id, nextUpdate = nextUpdate, fetchInterval = interval)
}
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
val lowerBound = today.minusDays(GRACE_PERIOD)
val upperBound = today.plusDays(GRACE_PERIOD)
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair<Long, Long> {
val today = localDateTime.atStartOfDayIn(timeZone)
val lowerBound = today - GRACE_PERIOD.days
val upperBound = today + GRACE_PERIOD.days
return Pair(lowerBound.toEpochMilliseconds(), upperBound.toEpochMilliseconds())
}
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
internal fun calculateInterval(chapters: List<Chapter>, zone: TimeZone): Int {
val chapterWindow = if (chapters.size <= 8) 3 else 10
val uploadDates = chapters.asSequence()
.filter { it.dateUpload > 0L }
.sortedByDescending { it.dateUpload }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
.toLocalDate()
.atStartOfDay()
Instant.fromEpochMilliseconds(it.dateUpload)
.toLocalDateTime(zone)
.date
.atStartOfDayIn(zone)
}
.distinct()
.take(chapterWindow)
@@ -58,9 +66,10 @@ class FetchInterval(
val fetchDates = chapters.asSequence()
.sortedByDescending { it.dateFetch }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
.toLocalDate()
.atStartOfDay()
Instant.fromEpochMilliseconds(it.dateFetch)
.toLocalDateTime(zone)
.date
.atStartOfDayIn(zone)
}
.distinct()
.take(chapterWindow)
@@ -69,13 +78,13 @@ class FetchInterval(
val interval = when {
// Enough upload date from source
uploadDates.size >= 3 -> {
val ranges = uploadDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
ranges[(ranges.size - 1) / 2].toInt()
val ranges = uploadDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
ranges[(ranges.size - 1) / 2]
}
// Enough fetch date from client
fetchDates.size >= 3 -> {
val ranges = fetchDates.windowed(2).map { x -> x[1].until(x[0], ChronoUnit.DAYS) }.sorted()
ranges[(ranges.size - 1) / 2].toInt()
val ranges = fetchDates.windowed(2).map { x -> x[1].daysUntil(x[0], zone) }.sorted()
ranges[(ranges.size - 1) / 2]
}
// Default to 7 days
else -> 7
@@ -87,34 +96,34 @@ class FetchInterval(
private fun calculateNextUpdate(
manga: Manga,
interval: Int,
dateTime: ZonedDateTime,
dateTime: LocalDateTime,
timeZone: TimeZone,
window: Pair<Long, Long>,
): Long {
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
return manga.nextUpdate
}
val latestDate = ZonedDateTime.ofInstant(
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
dateTime.zone,
)
.toLocalDate()
.atStartOfDay()
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
val cycle = timeSinceLatest.floorDiv(
val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now()
val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays
val cycle = daysSinceLatest.floorDiv(
interval.absoluteValue.takeIf { interval < 0 }
?: increaseInterval(interval, timeSinceLatest, increaseWhenOver = 10),
?: increaseInterval(interval, daysSinceLatest, increaseWhenOver = 10),
)
return latestDate.plusDays((cycle + 1) * interval.absoluteValue.toLong()).toEpochSecond(dateTime.offset) * 1000
val offsetDays = ((cycle + 1) * interval.absoluteValue.toLong()).days
return latestDate.plus(offsetDays).toEpochMilliseconds()
}
private fun increaseInterval(delta: Int, timeSinceLatest: Int, increaseWhenOver: Int): Int {
private fun increaseInterval(delta: Int, daysSinceLatest: Long, increaseWhenOver: Int): Int {
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
// double delta again if missed more than 9 check in new delta
val cycle = timeSinceLatest.floorDiv(delta) + 1
val cycle = daysSinceLatest.floorDiv(delta) + 1
return if (cycle > increaseWhenOver) {
increaseInterval(delta * 2, timeSinceLatest, increaseWhenOver)
increaseInterval(delta * 2, daysSinceLatest, increaseWhenOver)
} else {
delta
}
@@ -10,7 +10,7 @@ import kotlinx.serialization.json.JsonObject
import mihon.core.common.extensions.EMPTY
import tachiyomi.core.common.preference.TriState
import java.io.ObjectStreamException
import java.time.Instant
import kotlin.time.Instant
import java.io.Serializable as JavaSerializable
@SuppressLint("UnsafeOptInUsageError")
@@ -47,7 +47,7 @@ data class Manga(
val expectedNextUpdate: Instant?
get() = nextUpdate
.takeIf { status != SManga.COMPLETED.toLong() }
?.let { Instant.ofEpochMilli(it) }
?.let { Instant.fromEpochMilliseconds(it) }
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
@@ -3,7 +3,7 @@ package tachiyomi.domain.updates.interactor
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.repository.UpdatesRepository
import java.time.Instant
import kotlin.time.Instant
class GetUpdates(
private val repository: UpdatesRepository,
@@ -21,7 +21,7 @@ class GetUpdates(
hideExcludedScanlators: Boolean,
): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(
instant.toEpochMilli(),
instant.toEpochMilliseconds(),
limit = 500,
unread = unread,
started = started,