mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Store every date in the database as an Instant
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user