fix:conflict.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh
2023-08-03 01:13:39 +10:00
328 changed files with 37229 additions and 22851 deletions

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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())
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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,
)
}

View File

@@ -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)
}
}