Store every date in the database as an Instant

This commit is contained in:
AntsyLich
2026-10-10 17:37:43 +06:00
parent 8972882a34
commit 44c1e1915d
68 changed files with 599 additions and 284 deletions
@@ -3,7 +3,7 @@ package tachiyomi.domain.backup.model
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
import java.util.Date
import kotlin.time.Instant
data class RestoredManga(
val manga: Manga,
@@ -16,6 +16,6 @@ data class RestoredManga(
data class RestoredHistory(
val chapterUrl: String,
val readAt: Date?,
val readAt: Instant?,
val readDuration: Long,
)
@@ -2,6 +2,7 @@ package tachiyomi.domain.chapter.model
import kotlinx.serialization.json.JsonObject
import mihon.core.common.extensions.EMPTY
import kotlin.time.Instant
data class Chapter(
val id: Long,
@@ -9,11 +10,11 @@ data class Chapter(
val read: Boolean,
val bookmark: Boolean,
val lastPageRead: Long,
val dateFetch: Long,
val dateFetch: Instant,
val sourceOrder: Long,
val url: String,
val name: String,
val dateUpload: Long,
val dateUpload: Instant?,
val chapterNumber: Double,
val scanlator: String?,
val memo: JsonObject,
@@ -38,11 +39,11 @@ data class Chapter(
read = false,
bookmark = false,
lastPageRead = 0,
dateFetch = 0,
dateFetch = Instant.fromEpochMilliseconds(0),
sourceOrder = 0,
url = "",
name = "",
dateUpload = -1,
dateUpload = null,
chapterNumber = -1.0,
scanlator = null,
memo = JsonObject.EMPTY,
@@ -1,13 +1,14 @@
package tachiyomi.domain.chapter.model
import kotlinx.serialization.json.JsonObject
import kotlin.time.Instant
data class ChapterRemoteUpdate(
val id: Long,
val name: String,
val scanlator: String?,
val chapterNumber: Double,
val dateUpload: Long?,
val dateUpload: Instant?,
val sourceOrder: Long,
val memo: JsonObject,
)
@@ -1,12 +1,13 @@
package tachiyomi.domain.chapter.model
import mihon.domain.common.PartialUpdate
import kotlin.time.Instant
class ChapterUpdate(val id: Long, block: ChapterUpdate.() -> Unit) : PartialUpdate() {
var read: Boolean? by field(null)
var bookmark: Boolean? by field(null)
var lastPageRead: Long? by field(null)
var dateFetch: Long? by field(null)
var dateFetch: Instant? by field(null)
init {
block()
@@ -21,8 +21,8 @@ fun getChapterSort(
false -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) }
}
Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) {
true -> { c1, c2 -> c2.dateUpload.compareTo(c1.dateUpload) }
false -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
true -> { c1, c2 -> compareValues(c2.dateUpload, c1.dateUpload) }
false -> { c1, c2 -> compareValues(c1.dateUpload, c2.dateUpload) }
}
Manga.CHAPTER_SORTING_ALPHABET -> when (sortDescending) {
true -> { c1, c2 -> c2.name.compareToWithCollator(c1.name) }
@@ -1,11 +1,11 @@
package tachiyomi.domain.history.model
import java.util.Date
import kotlin.time.Instant
data class History(
val id: Long,
val chapterId: Long,
val readAt: Date?,
val readAt: Instant?,
val readDuration: Long,
) {
companion object {
@@ -1,9 +1,9 @@
package tachiyomi.domain.history.model
import java.util.Date
import kotlin.time.Instant
data class HistoryUpdate(
val chapterId: Long,
val readAt: Date,
val readAt: Instant,
val sessionReadDuration: Long,
)
@@ -1,7 +1,7 @@
package tachiyomi.domain.history.model
import tachiyomi.domain.manga.model.MangaCover
import java.util.Date
import kotlin.time.Instant
data class HistoryWithRelations(
val id: Long,
@@ -9,7 +9,7 @@ data class HistoryWithRelations(
val mangaId: Long,
val title: String,
val chapterNumber: Double,
val readAt: Date?,
val readAt: Instant?,
val readDuration: Long,
val coverData: MangaCover,
)
@@ -1,6 +1,7 @@
package tachiyomi.domain.library.model
import tachiyomi.domain.manga.model.Manga
import kotlin.time.Instant
data class LibraryManga(
val manga: Manga,
@@ -8,9 +9,9 @@ data class LibraryManga(
val totalChapters: Long,
val readCount: Long,
val bookmarkCount: Long,
val latestUpload: Long,
val chapterFetchedAt: Long,
val lastRead: Long,
val latestUpload: Instant?,
val chapterFetchedAt: Instant?,
val lastRead: Instant?,
) {
val id: Long = manga.id
@@ -15,6 +15,7 @@ import tachiyomi.domain.manga.model.MangaUpdate
import kotlin.math.absoluteValue
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Instant
@Inject
@@ -26,17 +27,13 @@ class FetchInterval(
manga: Manga,
dateTime: LocalDateTime,
timeZone: TimeZone,
window: Pair<Long, Long>,
window: ClosedRange<Instant>?,
): MangaUpdate {
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
zone = timeZone,
)
val currentWindow = if (window.first == 0L && window.second == 0L) {
getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
} else {
window
}
val currentWindow = window ?: getWindow(Clock.System.now().toLocalDateTime(timeZone).date, timeZone)
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, timeZone, currentWindow)
return MangaUpdate(manga.id) {
@@ -45,22 +42,19 @@ class FetchInterval(
}
}
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): Pair<Long, Long> {
fun getWindow(localDateTime: LocalDate, timeZone: TimeZone): ClosedRange<Instant> {
val today = localDateTime.atStartOfDayIn(timeZone)
val lowerBound = today - GRACE_PERIOD.days
val upperBound = today + GRACE_PERIOD.days
return Pair(lowerBound.toEpochMilliseconds(), upperBound.toEpochMilliseconds())
return (today - GRACE_PERIOD.days)..(today + GRACE_PERIOD.days)
}
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 }
.mapNotNull { it.dateUpload }
.sortedDescending()
.map {
Instant.fromEpochMilliseconds(it.dateUpload)
.toLocalDateTime(zone)
it.toLocalDateTime(zone)
.date
.atStartOfDayIn(zone)
}
@@ -69,10 +63,10 @@ class FetchInterval(
.toList()
val fetchDates = chapters.asSequence()
.sortedByDescending { it.dateFetch }
.map { it.dateFetch }
.sortedDescending()
.map {
Instant.fromEpochMilliseconds(it.dateFetch)
.toLocalDateTime(zone)
it.toLocalDateTime(zone)
.date
.atStartOfDayIn(zone)
}
@@ -103,13 +97,13 @@ class FetchInterval(
interval: Int,
dateTime: LocalDateTime,
timeZone: TimeZone,
window: Pair<Long, Long>,
): Long {
if (manga.nextUpdate in window.first.rangeTo(window.second + 1)) {
window: ClosedRange<Instant>,
): Instant {
if (manga.nextUpdate != null && manga.nextUpdate in window.start..window.endInclusive + 1.milliseconds) {
return manga.nextUpdate
}
val instant = if (manga.lastUpdate > 0) Instant.fromEpochMilliseconds(manga.lastUpdate) else Clock.System.now()
val instant = manga.lastUpdate ?: Clock.System.now()
val latestDate = instant.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
val daysSinceLatest = (dateTime.toInstant(timeZone) - latestDate).inWholeDays
@@ -119,7 +113,7 @@ class FetchInterval(
)
val offsetDays = ((cycle + 1) * interval.absoluteValue.toLong()).days
return latestDate.plus(offsetDays).toEpochMilliseconds()
return latestDate.plus(offsetDays)
}
private fun increaseInterval(delta: Int, daysSinceLatest: Long, increaseWhenOver: Int): Int {
@@ -19,13 +19,13 @@ import java.io.Serializable as JavaSerializable
data class Manga(
val id: Long,
val source: Long,
val lastUpdate: Long,
val nextUpdate: Long,
val lastUpdate: Instant?,
val nextUpdate: Instant?,
val fetchInterval: Int,
val favoriteAt: Long?,
val favoriteAt: Instant?,
val viewerFlags: Long,
val chapterFlags: Long,
val coverLastModified: Long,
val coverLastModified: Instant?,
val url: String,
val title: String,
val artist: String?,
@@ -44,9 +44,7 @@ data class Manga(
get() = favoriteAt != null
val expectedNextUpdate: Instant?
get() = nextUpdate
.takeIf { status != SManga.COMPLETED.toLong() }
?.let { Instant.fromEpochMilliseconds(it) }
get() = nextUpdate?.takeIf { status != SManga.COMPLETED.toLong() }
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
@@ -111,18 +109,23 @@ data class Manga(
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
const val CHAPTER_DISPLAY_MASK = 0x00100000L
/**
* [favoriteAt] of an entry added to the library before the time it was added was recorded.
*/
val UNKNOWN_FAVORITE_AT = Instant.fromEpochMilliseconds(0)
fun create() = Manga(
id = -1L,
url = "",
title = "",
source = -1L,
lastUpdate = 0L,
nextUpdate = 0L,
lastUpdate = null,
nextUpdate = null,
fetchInterval = 0,
favoriteAt = null,
viewerFlags = 0L,
chapterFlags = 0L,
coverLastModified = 0L,
coverLastModified = null,
artist = null,
author = null,
description = null,
@@ -1,5 +1,7 @@
package tachiyomi.domain.manga.model
import kotlin.time.Instant
/**
* Contains the required data for MangaCoverFetcher
*/
@@ -8,7 +10,7 @@ data class MangaCover(
val sourceId: Long,
val isMangaFavorite: Boolean,
val url: String?,
val lastModified: Long,
val lastModified: Instant?,
)
fun Manga.asMangaCover(): MangaCover {
@@ -2,6 +2,7 @@ package tachiyomi.domain.manga.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.json.JsonObject
import kotlin.time.Instant
data class MangaRemoteUpdate(
val id: Long,
@@ -15,5 +16,5 @@ data class MangaRemoteUpdate(
val updateStrategy: UpdateStrategy,
val memo: JsonObject,
val initialized: Boolean,
val coverLastModified: Long?,
val coverLastModified: Instant?,
)
@@ -1,15 +1,16 @@
package tachiyomi.domain.manga.model
import mihon.domain.common.PartialUpdate
import kotlin.time.Instant
class MangaUpdate(val id: Long, block: MangaUpdate.() -> Unit) : PartialUpdate() {
var favoriteAt: Long? by field(null)
var lastUpdate: Long? by field(null)
var nextUpdate: Long? by field(null)
var favoriteAt: Instant? by field(null)
var lastUpdate: Instant? by field(null)
var nextUpdate: Instant? by field(null)
var fetchInterval: Int? by field(null)
var viewerFlags: Long? by field(null)
var chapterFlags: Long? by field(null)
var coverLastModified: Long? by field(null)
var coverLastModified: Instant? by field(null)
var notes: String? by field(null)
init {
@@ -1,6 +1,7 @@
package tachiyomi.domain.track.model
import java.io.Serializable
import kotlin.time.Instant
data class Track(
val id: Long,
@@ -14,7 +15,7 @@ data class Track(
val status: Long,
val score: Double,
val remoteUrl: String,
val startDate: Long,
val finishDate: Long,
val startDate: Instant?,
val finishDate: Instant?,
val private: Boolean,
) : Serializable
@@ -11,7 +11,7 @@ class GetUpdates(
private val repository: UpdatesRepository,
) {
suspend fun await(read: Boolean, after: Long): List<UpdatesWithRelations> {
suspend fun await(read: Boolean, after: Instant): List<UpdatesWithRelations> {
return repository.awaitWithRead(read, after, limit = 500)
}
@@ -25,7 +25,7 @@ class GetUpdates(
excludedCategories: List<Long>,
): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(
instant.toEpochMilliseconds(),
instant,
limit = 500,
unread = unread,
started = started,
@@ -36,7 +36,7 @@ class GetUpdates(
)
}
fun subscribe(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
fun subscribe(read: Boolean, after: Instant): Flow<List<UpdatesWithRelations>> {
return repository.subscribeWithRead(read, after, limit = 500)
}
}
@@ -1,6 +1,7 @@
package tachiyomi.domain.updates.model
import tachiyomi.domain.manga.model.MangaCover
import kotlin.time.Instant
data class UpdatesWithRelations(
val mangaId: Long,
@@ -13,6 +14,6 @@ data class UpdatesWithRelations(
val bookmark: Boolean,
val lastPageRead: Long,
val sourceId: Long,
val dateFetch: Long,
val dateFetch: Instant,
val coverData: MangaCover,
)
@@ -2,13 +2,14 @@ package tachiyomi.domain.updates.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.updates.model.UpdatesWithRelations
import kotlin.time.Instant
interface UpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations>
suspend fun awaitWithRead(read: Boolean, after: Instant, limit: Long): List<UpdatesWithRelations>
fun subscribeAll(
after: Long,
after: Instant,
limit: Long,
unread: Boolean?,
started: Boolean?,
@@ -18,5 +19,5 @@ interface UpdatesRepository {
excludedCategories: List<Long>,
): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Instant, limit: Long): Flow<List<UpdatesWithRelations>>
}
@@ -12,8 +12,7 @@ import tachiyomi.domain.chapter.model.Chapter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlin.time.Instant
@Execution(ExecutionMode.CONCURRENT)
class FetchIntervalTest {
@@ -21,8 +20,8 @@ class FetchIntervalTest {
private val testTime = LocalDateTime.parse("2020-01-01T00:00:00")
private val testTimeZone = TimeZone.UTC
private var chapter = Chapter.create().copy(
dateFetch = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
dateUpload = testTime.toInstant(testTimeZone).toEpochMilliseconds(),
dateFetch = testTime.toInstant(testTimeZone),
dateUpload = testTime.toInstant(testTimeZone),
)
private val fetchInterval = FetchInterval(mockk())
@@ -35,7 +34,7 @@ class FetchIntervalTest {
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 7
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
it.copy(dateUpload = 0L)
it.copy(dateUpload = null)
}
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 7
}
@@ -119,7 +118,7 @@ class FetchIntervalTest {
fetchInterval.calculateInterval(chaptersWithUploadDate, testTimeZone) shouldBe 1
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
it.copy(dateUpload = 0L)
it.copy(dateUpload = null)
}
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testTimeZone) shouldBe 1
}
@@ -133,10 +132,10 @@ class FetchIntervalTest {
}
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.toInstant(testTimeZone).plus(duration).toEpochMilliseconds()
val newTime = testTime.toInstant(testTimeZone) + duration
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
private fun List<Chapter>.lastUploadDate() =
last().dateUpload.toDuration(DurationUnit.MILLISECONDS)
last().dateUpload!! - Instant.fromEpochMilliseconds(0)
}