mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-25 20:40:41 +02:00
@@ -37,8 +37,8 @@ class CreateCategoryWithName(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ class DeleteCategory(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ class RenameCategory(
|
||||
|
||||
suspend fun await(category: Category, name: String) = await(category.id, name)
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ class ReorderCategory(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object Unchanged : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data object Unchanged : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
|
||||
private enum class MoveTo {
|
||||
|
||||
@@ -17,8 +17,8 @@ class UpdateCategory(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class Error(val error: Exception) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data class Error(val error: Exception) : Result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ data class Chapter(
|
||||
val url: String,
|
||||
val name: String,
|
||||
val dateUpload: Long,
|
||||
val chapterNumber: Float,
|
||||
val chapterNumber: Double,
|
||||
val scanlator: String?,
|
||||
val lastModifiedAt: Long,
|
||||
) {
|
||||
@@ -30,7 +30,7 @@ data class Chapter(
|
||||
url = "",
|
||||
name = "",
|
||||
dateUpload = -1,
|
||||
chapterNumber = -1f,
|
||||
chapterNumber = -1.0,
|
||||
scanlator = null,
|
||||
lastModifiedAt = 0,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ data class ChapterUpdate(
|
||||
val url: String? = null,
|
||||
val name: String? = null,
|
||||
val dateUpload: Long? = null,
|
||||
val chapterNumber: Float? = null,
|
||||
val chapterNumber: Double? = null,
|
||||
val scanlator: String? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ object ChapterRecognition {
|
||||
*/
|
||||
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
|
||||
|
||||
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Float? = null): Float {
|
||||
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double {
|
||||
// If chapter number is known return.
|
||||
if (chapterNumber != null && (chapterNumber == -2f || chapterNumber > -1f)) {
|
||||
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
|
||||
return chapterNumber
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ object ChapterRecognition {
|
||||
// Take the first number encountered.
|
||||
number.find(name)?.let { return getChapterNumberFromMatch(it) }
|
||||
|
||||
return chapterNumber ?: -1f
|
||||
return chapterNumber ?: -1.0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,9 +65,9 @@ object ChapterRecognition {
|
||||
* @param match result of regex
|
||||
* @return chapter number if found else null
|
||||
*/
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Float {
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Double {
|
||||
return match.let {
|
||||
val initial = it.groups[1]?.value?.toFloat()!!
|
||||
val initial = it.groups[1]?.value?.toDouble()!!
|
||||
val subChapterDecimal = it.groups[2]?.value
|
||||
val subChapterAlpha = it.groups[3]?.value
|
||||
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
|
||||
@@ -81,22 +81,22 @@ object ChapterRecognition {
|
||||
* @param alpha alpha value of regex
|
||||
* @return decimal/alpha float value
|
||||
*/
|
||||
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
|
||||
private fun checkForDecimal(decimal: String?, alpha: String?): Double {
|
||||
if (!decimal.isNullOrEmpty()) {
|
||||
return decimal.toFloat()
|
||||
return decimal.toDouble()
|
||||
}
|
||||
|
||||
if (!alpha.isNullOrEmpty()) {
|
||||
if (alpha.contains("extra")) {
|
||||
return .99f
|
||||
return 0.99
|
||||
}
|
||||
|
||||
if (alpha.contains("omake")) {
|
||||
return .98f
|
||||
return 0.98
|
||||
}
|
||||
|
||||
if (alpha.contains("special")) {
|
||||
return .97f
|
||||
return 0.97
|
||||
}
|
||||
|
||||
val trimmedAlpha = alpha.trimStart('.')
|
||||
@@ -105,15 +105,15 @@ object ChapterRecognition {
|
||||
}
|
||||
}
|
||||
|
||||
return .0f
|
||||
return 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* x.a -> x.1, x.b -> x.2, etc
|
||||
*/
|
||||
private fun parseAlphaPostFix(alpha: Char): Float {
|
||||
private fun parseAlphaPostFix(alpha: Char): Double {
|
||||
val number = alpha.code - ('a'.code - 1)
|
||||
if (number >= 10) return 0f
|
||||
return number / 10f
|
||||
if (number >= 10) return 0.0
|
||||
return number / 10.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ package tachiyomi.domain.chapter.service
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import kotlin.math.floor
|
||||
|
||||
fun List<Float>.missingChaptersCount(): Int {
|
||||
fun List<Double>.missingChaptersCount(): Int {
|
||||
if (this.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val chapters = this
|
||||
// Ignore unknown chapter numbers
|
||||
.filterNot { it == -1f }
|
||||
.filterNot { it == -1.0 }
|
||||
// Convert to integers, as we cannot check if 16.5 is missing
|
||||
.map(Float::toInt)
|
||||
.map(Double::toInt)
|
||||
// Only keep unique chapters so that -1 or 16 are not counted multiple times
|
||||
.distinct()
|
||||
.sorted()
|
||||
@@ -43,7 +43,7 @@ fun calculateChapterGap(higherChapter: Chapter?, lowerChapter: Chapter?): Int {
|
||||
return calculateChapterGap(higherChapter.chapterNumber, lowerChapter.chapterNumber)
|
||||
}
|
||||
|
||||
fun calculateChapterGap(higherChapterNumber: Float, lowerChapterNumber: Float): Int {
|
||||
if (higherChapterNumber < 0f || lowerChapterNumber < 0f) return 0
|
||||
fun calculateChapterGap(higherChapterNumber: Double, lowerChapterNumber: Double): Int {
|
||||
if (higherChapterNumber < 0.0 || lowerChapterNumber < 0.0) return 0
|
||||
return floor(higherChapterNumber).toInt() - floor(lowerChapterNumber).toInt() - 1
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tachiyomi.domain.history.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.model.History
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
|
||||
@@ -8,6 +9,10 @@ class GetHistory(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): List<History> {
|
||||
return repository.getHistoryByMangaId(mangaId)
|
||||
}
|
||||
|
||||
fun subscribe(query: String): Flow<List<HistoryWithRelations>> {
|
||||
return repository.getHistory(query)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ data class HistoryWithRelations(
|
||||
val chapterId: Long,
|
||||
val mangaId: Long,
|
||||
val title: String,
|
||||
val chapterNumber: Float,
|
||||
val chapterNumber: Double,
|
||||
val readAt: Date?,
|
||||
val readDuration: Long,
|
||||
val coverData: MangaCover,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tachiyomi.domain.history.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.model.History
|
||||
import tachiyomi.domain.history.model.HistoryUpdate
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
|
||||
@@ -12,6 +13,8 @@ interface HistoryRepository {
|
||||
|
||||
suspend fun getTotalReadDuration(): Long
|
||||
|
||||
suspend fun getHistoryByMangaId(mangaId: Long): List<History>
|
||||
|
||||
suspend fun resetHistory(historyId: Long)
|
||||
|
||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package tachiyomi.domain.library.model
|
||||
|
||||
sealed class LibraryDisplayMode {
|
||||
sealed interface LibraryDisplayMode {
|
||||
|
||||
object CompactGrid : LibraryDisplayMode()
|
||||
object ComfortableGrid : LibraryDisplayMode()
|
||||
object List : LibraryDisplayMode()
|
||||
object CoverOnlyGrid : LibraryDisplayMode()
|
||||
data object CompactGrid : LibraryDisplayMode
|
||||
data object ComfortableGrid : LibraryDisplayMode
|
||||
data object List : LibraryDisplayMode
|
||||
data object CoverOnlyGrid : LibraryDisplayMode
|
||||
|
||||
object Serializer {
|
||||
fun deserialize(serialized: String): LibraryDisplayMode {
|
||||
|
||||
@@ -22,14 +22,14 @@ data class LibrarySort(
|
||||
|
||||
override val mask: Long = 0b00111100L
|
||||
|
||||
object Alphabetical : Type(0b00000000)
|
||||
object LastRead : Type(0b00000100)
|
||||
object LastUpdate : Type(0b00001000)
|
||||
object UnreadCount : Type(0b00001100)
|
||||
object TotalChapters : Type(0b00010000)
|
||||
object LatestChapter : Type(0b00010100)
|
||||
object ChapterFetchDate : Type(0b00011000)
|
||||
object DateAdded : Type(0b00011100)
|
||||
data object Alphabetical : Type(0b00000000)
|
||||
data object LastRead : Type(0b00000100)
|
||||
data object LastUpdate : Type(0b00001000)
|
||||
data object UnreadCount : Type(0b00001100)
|
||||
data object TotalChapters : Type(0b00010000)
|
||||
data object LatestChapter : Type(0b00010100)
|
||||
data object ChapterFetchDate : Type(0b00011000)
|
||||
data object DateAdded : Type(0b00011100)
|
||||
|
||||
companion object {
|
||||
fun valueOf(flag: Long): Type {
|
||||
@@ -44,8 +44,8 @@ data class LibrarySort(
|
||||
|
||||
override val mask: Long = 0b01000000L
|
||||
|
||||
object Ascending : Direction(0b01000000)
|
||||
object Descending : Direction(0b00000000)
|
||||
data object Ascending : Direction(0b01000000)
|
||||
data object Descending : Direction(0b00000000)
|
||||
|
||||
companion object {
|
||||
fun valueOf(flag: Long): Direction {
|
||||
|
||||
@@ -38,9 +38,6 @@ class LibraryPreferences(
|
||||
),
|
||||
)
|
||||
|
||||
fun leadingExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
|
||||
fun followingExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
|
||||
|
||||
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
|
||||
|
||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||
|
||||
@@ -7,7 +7,7 @@ class GetDuplicateLibraryManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(title: String): Manga? {
|
||||
return mangaRepository.getDuplicateLibraryManga(title.lowercase())
|
||||
suspend fun await(manga: Manga): List<Manga> {
|
||||
return mangaRepository.getDuplicateLibraryManga(manga.id, manga.title.lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package tachiyomi.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
||||
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.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
const val MAX_FETCH_INTERVAL = 28
|
||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||
|
||||
class SetFetchInterval(
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
) {
|
||||
|
||||
suspend fun toMangaUpdateOrNull(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): MangaUpdate? {
|
||||
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||
getWindow(ZonedDateTime.now())
|
||||
} else {
|
||||
window
|
||||
}
|
||||
val chapters = getChapterByMangaId.await(manga.id)
|
||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||
|
||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
||||
null
|
||||
} else {
|
||||
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(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val upperBound = lowerBound.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.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
|
||||
}
|
||||
|
||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||
manga.fetchInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(
|
||||
interval.absoluteValue.takeIf { interval < 0 }
|
||||
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
|
||||
)
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||
} else {
|
||||
manga.nextUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package tachiyomi.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.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()
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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 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)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ data class Manga(
|
||||
val favorite: Boolean,
|
||||
val lastUpdate: Long,
|
||||
val nextUpdate: Long,
|
||||
val calculateInterval: Int,
|
||||
val fetchInterval: Int,
|
||||
val dateAdded: Long,
|
||||
val viewerFlags: Long,
|
||||
val chapterFlags: Long,
|
||||
@@ -99,7 +99,7 @@ data class Manga(
|
||||
favorite = false,
|
||||
lastUpdate = 0L,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0,
|
||||
fetchInterval = 0,
|
||||
dateAdded = 0L,
|
||||
viewerFlags = 0L,
|
||||
chapterFlags = 0L,
|
||||
|
||||
@@ -8,7 +8,7 @@ data class MangaUpdate(
|
||||
val favorite: Boolean? = null,
|
||||
val lastUpdate: Long? = null,
|
||||
val nextUpdate: Long? = null,
|
||||
val calculateInterval: Int? = null,
|
||||
val fetchInterval: Int? = null,
|
||||
val dateAdded: Long? = null,
|
||||
val viewerFlags: Long? = null,
|
||||
val chapterFlags: Long? = null,
|
||||
@@ -32,7 +32,7 @@ fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate,
|
||||
nextUpdate = nextUpdate,
|
||||
calculateInterval = calculateInterval,
|
||||
fetchInterval = fetchInterval,
|
||||
dateAdded = dateAdded,
|
||||
viewerFlags = viewerFlags,
|
||||
chapterFlags = chapterFlags,
|
||||
|
||||
@@ -23,7 +23,7 @@ interface MangaRepository {
|
||||
|
||||
fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
|
||||
|
||||
suspend fun getDuplicateLibraryManga(title: String): Manga?
|
||||
suspend fun getDuplicateLibraryManga(id: Long, title: String): List<Manga>
|
||||
|
||||
suspend fun resetViewerFlags(): Boolean
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ class GetApplicationRelease(
|
||||
val forceCheck: Boolean = false,
|
||||
)
|
||||
|
||||
sealed class Result {
|
||||
class NewUpdate(val release: Release) : Result()
|
||||
object NoNewUpdate : Result()
|
||||
object ThirdPartyInstallation : Result()
|
||||
sealed interface Result {
|
||||
data class NewUpdate(val release: Release) : Result
|
||||
data object NoNewUpdate : Result
|
||||
data object ThirdPartyInstallation : Result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package tachiyomi.domain.source.model
|
||||
|
||||
sealed class Pin(val code: Int) {
|
||||
object Unpinned : Pin(0b00)
|
||||
object Pinned : Pin(0b01)
|
||||
object Actual : Pin(0b10)
|
||||
data object Unpinned : Pin(0b00)
|
||||
data object Pinned : Pin(0b01)
|
||||
data object Actual : Pin(0b10)
|
||||
}
|
||||
|
||||
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||
|
||||
@@ -10,7 +10,7 @@ data class Track(
|
||||
val lastChapterRead: Double,
|
||||
val totalChapters: Long,
|
||||
val status: Long,
|
||||
val score: Float,
|
||||
val score: Double,
|
||||
val remoteUrl: String,
|
||||
val startDate: Long,
|
||||
val finishDate: Long,
|
||||
|
||||
@@ -12,152 +12,152 @@ class ChapterRecognitionTest {
|
||||
fun `Basic Ch prefix`() {
|
||||
val mangaTitle = "Mokushiroku Alice"
|
||||
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4: Misrepresentation", 4f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4: Misrepresentation", 4.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Basic Ch prefix with space after period`() {
|
||||
val mangaTitle = "Mokushiroku Alice"
|
||||
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation", 4f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation", 4.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Basic Ch prefix with decimal`() {
|
||||
val mangaTitle = "Mokushiroku Alice"
|
||||
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation", 4.1f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation", 4.4f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation", 4.1)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation", 4.4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Basic Ch prefix with alpha postfix`() {
|
||||
val mangaTitle = "Mokushiroku Alice"
|
||||
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation", 4.1f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation", 4.2f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation", 4.99f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation", 4.1)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation", 4.2)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation", 4.99)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Name containing one number`() {
|
||||
val mangaTitle = "Bleach"
|
||||
|
||||
assertChapter(mangaTitle, "Bleach 567 Down With Snowwhite", 567f)
|
||||
assertChapter(mangaTitle, "Bleach 567 Down With Snowwhite", 567.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Name containing one number and decimal`() {
|
||||
val mangaTitle = "Bleach"
|
||||
|
||||
assertChapter(mangaTitle, "Bleach 567.1 Down With Snowwhite", 567.1f)
|
||||
assertChapter(mangaTitle, "Bleach 567.4 Down With Snowwhite", 567.4f)
|
||||
assertChapter(mangaTitle, "Bleach 567.1 Down With Snowwhite", 567.1)
|
||||
assertChapter(mangaTitle, "Bleach 567.4 Down With Snowwhite", 567.4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Name containing one number and alpha`() {
|
||||
val mangaTitle = "Bleach"
|
||||
|
||||
assertChapter(mangaTitle, "Bleach 567.a Down With Snowwhite", 567.1f)
|
||||
assertChapter(mangaTitle, "Bleach 567.b Down With Snowwhite", 567.2f)
|
||||
assertChapter(mangaTitle, "Bleach 567.extra Down With Snowwhite", 567.99f)
|
||||
assertChapter(mangaTitle, "Bleach 567.a Down With Snowwhite", 567.1)
|
||||
assertChapter(mangaTitle, "Bleach 567.b Down With Snowwhite", 567.2)
|
||||
assertChapter(mangaTitle, "Bleach 567.extra Down With Snowwhite", 567.99)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter containing manga title and number`() {
|
||||
val mangaTitle = "Solanin"
|
||||
|
||||
assertChapter(mangaTitle, "Solanin 028 Vol. 2", 28f)
|
||||
assertChapter(mangaTitle, "Solanin 028 Vol. 2", 28.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter containing manga title and number decimal`() {
|
||||
val mangaTitle = "Solanin"
|
||||
|
||||
assertChapter(mangaTitle, "Solanin 028.1 Vol. 2", 28.1f)
|
||||
assertChapter(mangaTitle, "Solanin 028.4 Vol. 2", 28.4f)
|
||||
assertChapter(mangaTitle, "Solanin 028.1 Vol. 2", 28.1)
|
||||
assertChapter(mangaTitle, "Solanin 028.4 Vol. 2", 28.4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter containing manga title and number alpha`() {
|
||||
val mangaTitle = "Solanin"
|
||||
|
||||
assertChapter(mangaTitle, "Solanin 028.a Vol. 2", 28.1f)
|
||||
assertChapter(mangaTitle, "Solanin 028.b Vol. 2", 28.2f)
|
||||
assertChapter(mangaTitle, "Solanin 028.extra Vol. 2", 28.99f)
|
||||
assertChapter(mangaTitle, "Solanin 028.a Vol. 2", 28.1)
|
||||
assertChapter(mangaTitle, "Solanin 028.b Vol. 2", 28.2)
|
||||
assertChapter(mangaTitle, "Solanin 028.extra Vol. 2", 28.99)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Extreme case`() {
|
||||
val mangaTitle = "Onepunch-Man"
|
||||
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028", 28f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028", 28.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Extreme case with decimal`() {
|
||||
val mangaTitle = "Onepunch-Man"
|
||||
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.1", 28.1f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.4", 28.4f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.1", 28.1)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.4", 28.4)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Extreme case with alpha`() {
|
||||
val mangaTitle = "Onepunch-Man"
|
||||
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.a", 28.1f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.b", 28.2f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.extra", 28.99f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.a", 28.1)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.b", 28.2)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.extra", 28.99)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter containing dot v2`() {
|
||||
val mangaTitle = "random"
|
||||
|
||||
assertChapter(mangaTitle, "Vol.1 Ch.5v.2: Alones", 5f)
|
||||
assertChapter(mangaTitle, "Vol.1 Ch.5v.2: Alones", 5.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Number in manga title`() {
|
||||
val mangaTitle = "Ayame 14"
|
||||
|
||||
assertChapter(mangaTitle, "Ayame 14 1 - The summer of 14", 1f)
|
||||
assertChapter(mangaTitle, "Ayame 14 1 - The summer of 14", 1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Space between ch x`() {
|
||||
val mangaTitle = "Mokushiroku Alice"
|
||||
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation", 4f)
|
||||
assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation", 4.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title with ch substring`() {
|
||||
val mangaTitle = "Ayame 14"
|
||||
|
||||
assertChapter(mangaTitle, "Vol.1 Ch.1: March 25 (First Day Cohabiting)", 1f)
|
||||
assertChapter(mangaTitle, "Vol.1 Ch.1: March 25 (First Day Cohabiting)", 1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter containing multiple zeros`() {
|
||||
val mangaTitle = "random"
|
||||
|
||||
assertChapter(mangaTitle, "Vol.001 Ch.003: Kaguya Doesn't Know Much", 3f)
|
||||
assertChapter(mangaTitle, "Vol.001 Ch.003: Kaguya Doesn't Know Much", 3.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter with version before number`() {
|
||||
val mangaTitle = "Onepunch-Man"
|
||||
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]", 86f)
|
||||
assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]", 86.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Version attached to chapter number`() {
|
||||
val mangaTitle = "Ansatsu Kyoushitsu"
|
||||
|
||||
assertChapter(mangaTitle, "Ansatsu Kyoushitsu 011v002: Assembly Time", 11f)
|
||||
assertChapter(mangaTitle, "Ansatsu Kyoushitsu 011v002: Assembly Time", 11.0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,99 +168,99 @@ class ChapterRecognitionTest {
|
||||
fun `Number after manga title with chapter in chapter title case`() {
|
||||
val mangaTitle = "Tokyo ESP"
|
||||
|
||||
assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027f)
|
||||
assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Unparseable chapter`() {
|
||||
val mangaTitle = "random"
|
||||
|
||||
assertChapter(mangaTitle, "Foo", -1f)
|
||||
assertChapter(mangaTitle, "Foo", -1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter with time in title`() {
|
||||
val mangaTitle = "random"
|
||||
|
||||
assertChapter(mangaTitle, "Fairy Tail 404: 00:00", 404f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404: 00:00", 404.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter with alpha without dot`() {
|
||||
val mangaTitle = "random"
|
||||
|
||||
assertChapter(mangaTitle, "Asu No Yoichi 19a", 19.1f)
|
||||
assertChapter(mangaTitle, "Asu No Yoichi 19a", 19.1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title containing extra and vol`() {
|
||||
val mangaTitle = "Fairy Tail"
|
||||
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.extravol002", 404.99f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 extravol002", 404.99f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.extravol002", 404.99)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 extravol002", 404.99)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title containing omake (japanese extra) and vol`() {
|
||||
val mangaTitle = "Fairy Tail"
|
||||
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.omakevol002", 404.98f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 omakevol002", 404.98f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.omakevol002", 404.98)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 omakevol002", 404.98)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title containing special and vol`() {
|
||||
val mangaTitle = "Fairy Tail"
|
||||
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.specialvol002", 404.97f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 specialvol002", 404.97f)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404.specialvol002", 404.97)
|
||||
assertChapter(mangaTitle, "Fairy Tail 404 specialvol002", 404.97)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title containing commas`() {
|
||||
val mangaTitle = "One Piece"
|
||||
|
||||
assertChapter(mangaTitle, "One Piece 300,a", 300.1f)
|
||||
assertChapter(mangaTitle, "One Piece Ch,123,extra", 123.99f)
|
||||
assertChapter(mangaTitle, "One Piece the sunny, goes swimming 024,005", 24.005f)
|
||||
assertChapter(mangaTitle, "One Piece 300,a", 300.1)
|
||||
assertChapter(mangaTitle, "One Piece Ch,123,extra", 123.99)
|
||||
assertChapter(mangaTitle, "One Piece the sunny, goes swimming 024,005", 24.005)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapter title containing hyphens`() {
|
||||
val mangaTitle = "Solo Leveling"
|
||||
|
||||
assertChapter(mangaTitle, "ch 122-a", 122.1f)
|
||||
assertChapter(mangaTitle, "Solo Leveling Ch.123-extra", 123.99f)
|
||||
assertChapter(mangaTitle, "Solo Leveling, 024-005", 24.005f)
|
||||
assertChapter(mangaTitle, "Ch.191-200 Read Online", 191.200f)
|
||||
assertChapter(mangaTitle, "ch 122-a", 122.1)
|
||||
assertChapter(mangaTitle, "Solo Leveling Ch.123-extra", 123.99)
|
||||
assertChapter(mangaTitle, "Solo Leveling, 024-005", 24.005)
|
||||
assertChapter(mangaTitle, "Ch.191-200 Read Online", 191.200)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapters containing season`() {
|
||||
assertChapter("D.I.C.E", "D.I.C.E[Season 001] Ep. 007", 7f)
|
||||
assertChapter("D.I.C.E", "D.I.C.E[Season 001] Ep. 007", 7.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapters in format sx - chapter xx`() {
|
||||
assertChapter("The Gamer", "S3 - Chapter 20", 20f)
|
||||
assertChapter("The Gamer", "S3 - Chapter 20", 20.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapters ending with s`() {
|
||||
assertChapter("One Outs", "One Outs 001", 1f)
|
||||
assertChapter("One Outs", "One Outs 001", 1.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Chapters containing ordinals`() {
|
||||
val mangaTitle = "The Sister of the Woods with a Thousand Young"
|
||||
|
||||
assertChapter(mangaTitle, "The 1st Night", 1f)
|
||||
assertChapter(mangaTitle, "The 2nd Night", 2f)
|
||||
assertChapter(mangaTitle, "The 3rd Night", 3f)
|
||||
assertChapter(mangaTitle, "The 4th Night", 4f)
|
||||
assertChapter(mangaTitle, "The 1st Night", 1.0)
|
||||
assertChapter(mangaTitle, "The 2nd Night", 2.0)
|
||||
assertChapter(mangaTitle, "The 3rd Night", 3.0)
|
||||
assertChapter(mangaTitle, "The 4th Night", 4.0)
|
||||
}
|
||||
|
||||
private fun assertChapter(mangaTitle: String, name: String, expected: Float) {
|
||||
private fun assertChapter(mangaTitle: String, name: String, expected: Double) {
|
||||
ChapterRecognition.parseChapterNumber(mangaTitle, name) shouldBe expected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,47 +11,47 @@ class MissingChaptersTest {
|
||||
|
||||
@Test
|
||||
fun `missingChaptersCount returns 0 when empty list`() {
|
||||
emptyList<Float>().missingChaptersCount() shouldBe 0
|
||||
emptyList<Double>().missingChaptersCount() shouldBe 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missingChaptersCount returns 0 when all unknown chapter numbers`() {
|
||||
listOf(-1f, -1f, -1f).missingChaptersCount() shouldBe 0
|
||||
listOf(-1.0, -1.0, -1.0).missingChaptersCount() shouldBe 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missingChaptersCount handles repeated base chapter numbers`() {
|
||||
listOf(1f, 1.0f, 1.1f, 1.5f, 1.6f, 1.99f).missingChaptersCount() shouldBe 0
|
||||
listOf(1.0, 1.0, 1.1, 1.5, 1.6, 1.99).missingChaptersCount() shouldBe 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missingChaptersCount returns number of missing chapters`() {
|
||||
listOf(-1f, 1f, 2f, 2.2f, 4f, 6f, 10f, 11f).missingChaptersCount() shouldBe 5
|
||||
listOf(-1.0, 1.0, 2.0, 2.2, 4.0, 6.0, 10.0, 11.0).missingChaptersCount() shouldBe 5
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateChapterGap returns difference`() {
|
||||
calculateChapterGap(chapter(10f), chapter(9f)) shouldBe 0f
|
||||
calculateChapterGap(chapter(10f), chapter(8f)) shouldBe 1f
|
||||
calculateChapterGap(chapter(10f), chapter(8.5f)) shouldBe 1f
|
||||
calculateChapterGap(chapter(10f), chapter(1.1f)) shouldBe 8f
|
||||
calculateChapterGap(chapter(10.0), chapter(9.0)) shouldBe 0f
|
||||
calculateChapterGap(chapter(10.0), chapter(8.0)) shouldBe 1f
|
||||
calculateChapterGap(chapter(10.0), chapter(8.5)) shouldBe 1f
|
||||
calculateChapterGap(chapter(10.0), chapter(1.1)) shouldBe 8f
|
||||
|
||||
calculateChapterGap(10f, 9f) shouldBe 0f
|
||||
calculateChapterGap(10f, 8f) shouldBe 1f
|
||||
calculateChapterGap(10f, 8.5f) shouldBe 1f
|
||||
calculateChapterGap(10f, 1.1f) shouldBe 8f
|
||||
calculateChapterGap(10.0, 9.0) shouldBe 0f
|
||||
calculateChapterGap(10.0, 8.0) shouldBe 1f
|
||||
calculateChapterGap(10.0, 8.5) shouldBe 1f
|
||||
calculateChapterGap(10.0, 1.1) shouldBe 8f
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateChapterGap returns 0 if either are not valid chapter numbers`() {
|
||||
calculateChapterGap(chapter(-1f), chapter(10f)) shouldBe 0
|
||||
calculateChapterGap(chapter(99f), chapter(-1f)) shouldBe 0
|
||||
calculateChapterGap(chapter(-1.0), chapter(10.0)) shouldBe 0
|
||||
calculateChapterGap(chapter(99.0), chapter(-1.0)) shouldBe 0
|
||||
|
||||
calculateChapterGap(-1f, 10f) shouldBe 0
|
||||
calculateChapterGap(99f, -1f) shouldBe 0
|
||||
calculateChapterGap(-1.0, 10.0) shouldBe 0
|
||||
calculateChapterGap(99.0, -1.0) shouldBe 0
|
||||
}
|
||||
|
||||
private fun chapter(number: Float) = Chapter.create().copy(
|
||||
private fun chapter(number: Double) = Chapter.create().copy(
|
||||
chapterNumber = number,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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 SetFetchIntervalTest {
|
||||
|
||||
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 setFetchInterval = SetFetchInterval(mockk())
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setFetchInterval.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)
|
||||
}
|
||||
setFetchInterval.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)
|
||||
}
|
||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setFetchInterval.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)
|
||||
}
|
||||
setFetchInterval.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)
|
||||
}
|
||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setFetchInterval.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)
|
||||
}
|
||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user