chore: merge upstream.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh
2023-10-11 21:44:54 +11:00
312 changed files with 4467 additions and 3171 deletions

View File

@@ -16,11 +16,9 @@ class ReorderCategory(
private val mutex = Mutex()
suspend fun moveUp(category: Category): Result =
await(category, MoveTo.UP)
suspend fun moveUp(category: Category): Result = await(category, MoveTo.UP)
suspend fun moveDown(category: Category): Result =
await(category, MoveTo.DOWN)
suspend fun moveDown(category: Category): Result = await(category, MoveTo.DOWN)
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
mutex.withLock {
@@ -57,6 +55,27 @@ class ReorderCategory(
}
}
suspend fun sortAlphabetically() = withNonCancellableContext {
mutex.withLock {
val updates = categoryRepository.getAll()
.sortedBy { category -> category.name }
.mapIndexed { index, category ->
CategoryUpdate(
id = category.id,
order = index.toLong(),
)
}
try {
categoryRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
}
sealed interface Result {
data object Success : Result
data object Unchanged : Result

View File

@@ -28,7 +28,11 @@ class SetSortModeForCategory(
}
}
suspend fun await(category: Category?, type: LibrarySort.Type, direction: LibrarySort.Direction) {
suspend fun await(
category: Category?,
type: LibrarySort.Type,
direction: LibrarySort.Direction,
) {
await(category?.id, type, direction)
}
}

View File

@@ -30,7 +30,11 @@ object ChapterRecognition {
*/
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double {
fun parseChapterNumber(
mangaTitle: String,
chapterName: String,
chapterNumber: Double? = null,
): Double {
// If chapter number is known return.
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
return chapterNumber

View File

@@ -3,7 +3,13 @@ package tachiyomi.domain.chapter.service
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int {
fun getChapterSort(
manga: Manga,
sortDescending: Boolean = manga.sortDescending(),
): (
Chapter,
Chapter,
) -> Int {
return when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> when (sortDescending) {
true -> { c1, c2 -> c1.sourceOrder.compareTo(c2.sourceOrder) }

View File

@@ -8,9 +8,15 @@ class DownloadPreferences(
private val preferenceStore: PreferenceStore,
) {
fun downloadsDirectory() = preferenceStore.getString("download_directory", folderProvider.path())
fun downloadsDirectory() = preferenceStore.getString(
"download_directory",
folderProvider.path(),
)
fun downloadOnlyOverWifi() = preferenceStore.getBoolean("pref_download_only_over_wifi_key", true)
fun downloadOnlyOverWifi() = preferenceStore.getBoolean(
"pref_download_only_over_wifi_key",
true,
)
fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
@@ -20,15 +26,27 @@ class DownloadPreferences(
fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false)
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean(
"pref_remove_after_marked_as_read_key",
false,
)
fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
fun removeExcludeCategories() = preferenceStore.getStringSet(
"remove_exclude_categories",
emptySet(),
)
fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet())
fun downloadNewChapterCategories() = preferenceStore.getStringSet(
"download_new_categories",
emptySet(),
)
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet("download_new_categories_exclude", emptySet())
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet(
"download_new_categories_exclude",
emptySet(),
)
}

View File

@@ -30,7 +30,11 @@ class GetNextChapters(
}
}
suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List<Chapter> {
suspend fun await(
mangaId: Long,
fromChapterId: Long,
onlyUnread: Boolean = true,
): List<Chapter> {
val chapters = await(mangaId, onlyUnread)
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)

View File

@@ -65,7 +65,18 @@ data class LibrarySort(
}
companion object {
val types by lazy { setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded) }
val types by lazy {
setOf(
Type.Alphabetical,
Type.LastRead,
Type.LastUpdate,
Type.UnreadCount,
Type.TotalChapters,
Type.LatestChapter,
Type.ChapterFetchDate,
Type.DateAdded,
)
}
val directions by lazy { setOf(Direction.Ascending, Direction.Descending) }
val default = LibrarySort(Type.Alphabetical, Direction.Ascending)

View File

@@ -11,9 +11,19 @@ class LibraryPreferences(
private val preferenceStore: PreferenceStore,
) {
fun displayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
fun displayMode() = preferenceStore.getObject(
"pref_display_mode_library",
LibraryDisplayMode.default,
LibraryDisplayMode.Serializer::serialize,
LibraryDisplayMode.Serializer::deserialize,
)
fun sortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize)
fun sortingMode() = preferenceStore.getObject(
"library_sorting_mode",
LibrarySort.default,
LibrarySort.Serializer::serialize,
LibrarySort.Serializer::deserialize,
)
fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0)
@@ -42,31 +52,64 @@ class LibraryPreferences(
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
fun showContinueReadingButton() = preferenceStore.getBoolean(
"display_continue_reading_button",
false,
)
// region Filter
fun filterDownloaded() = preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED)
fun filterDownloaded() = preferenceStore.getEnum(
"pref_filter_library_downloaded_v2",
TriState.DISABLED,
)
fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED)
fun filterStarted() = preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED)
fun filterStarted() = preferenceStore.getEnum(
"pref_filter_library_started_v2",
TriState.DISABLED,
)
fun filterBookmarked() = preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED)
fun filterBookmarked() = preferenceStore.getEnum(
"pref_filter_library_bookmarked_v2",
TriState.DISABLED,
)
fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED)
fun filterCompleted() = preferenceStore.getEnum(
"pref_filter_library_completed_v2",
TriState.DISABLED,
)
fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriState.DISABLED)
fun filterIntervalCustom() = preferenceStore.getEnum(
"pref_filter_library_interval_custom",
TriState.DISABLED,
)
fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriState.DISABLED)
fun filterIntervalLong() = preferenceStore.getEnum(
"pref_filter_library_interval_long",
TriState.DISABLED,
)
fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriState.DISABLED)
fun filterIntervalLate() = preferenceStore.getEnum(
"pref_filter_library_interval_late",
TriState.DISABLED,
)
fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriState.DISABLED)
fun filterIntervalDropped() = preferenceStore.getEnum(
"pref_filter_library_interval_dropped",
TriState.DISABLED,
)
fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriState.DISABLED)
fun filterIntervalPassed() = preferenceStore.getEnum(
"pref_filter_library_interval_passed",
TriState.DISABLED,
)
fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED)
fun filterTracking(id: Int) = preferenceStore.getEnum(
"pref_filter_library_tracked_${id}_v2",
TriState.DISABLED,
)
// endregion
@@ -97,24 +140,45 @@ class LibraryPreferences(
fun updateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
fun updateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
fun updateCategoriesExclude() = preferenceStore.getStringSet(
"library_update_categories_exclude",
emptySet(),
)
// endregion
// region Chapter
fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL)
fun filterChapterByRead() = preferenceStore.getLong(
"default_chapter_filter_by_read",
Manga.SHOW_ALL,
)
fun filterChapterByDownloaded() = preferenceStore.getLong("default_chapter_filter_by_downloaded", Manga.SHOW_ALL)
fun filterChapterByDownloaded() = preferenceStore.getLong(
"default_chapter_filter_by_downloaded",
Manga.SHOW_ALL,
)
fun filterChapterByBookmarked() = preferenceStore.getLong("default_chapter_filter_by_bookmarked", Manga.SHOW_ALL)
fun filterChapterByBookmarked() = preferenceStore.getLong(
"default_chapter_filter_by_bookmarked",
Manga.SHOW_ALL,
)
// and upload date
fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE)
fun sortChapterBySourceOrNumber() = preferenceStore.getLong(
"default_chapter_sort_by_source_or_number",
Manga.CHAPTER_SORTING_SOURCE,
)
fun displayChapterByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Manga.CHAPTER_DISPLAY_NAME)
fun displayChapterByNameOrNumber() = preferenceStore.getLong(
"default_chapter_display_by_name_or_number",
Manga.CHAPTER_DISPLAY_NAME,
)
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Manga.CHAPTER_SORT_DESC)
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong(
"default_chapter_sort_by_ascending_or_descending",
Manga.CHAPTER_SORT_DESC,
)
fun setChapterSettingsDefault(manga: Manga) {
filterChapterByRead().set(manga.unreadFilterRaw)
@@ -122,7 +186,9 @@ class LibraryPreferences(
filterChapterByBookmarked().set(manga.bookmarkedFilterRaw)
sortChapterBySourceOrNumber().set(manga.sorting)
displayChapterByNameOrNumber().set(manga.displayMode)
sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
sortChapterByAscendingOrDescending().set(
if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC,
)
}
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
@@ -131,9 +197,15 @@ class LibraryPreferences(
// region Swipe Actions
fun swipeToStartAction() = preferenceStore.getEnum("pref_chapter_swipe_end_action", ChapterSwipeAction.ToggleBookmark)
fun swipeToStartAction() = preferenceStore.getEnum(
"pref_chapter_swipe_end_action",
ChapterSwipeAction.ToggleBookmark,
)
fun swipeToEndAction() = preferenceStore.getEnum("pref_chapter_swipe_start_action", ChapterSwipeAction.ToggleRead)
fun swipeToEndAction() = preferenceStore.getEnum(
"pref_chapter_swipe_start_action",
ChapterSwipeAction.ToggleRead,
)
// endregion

View File

@@ -5,14 +5,12 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
const val MAX_FETCH_INTERVAL = 28
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
class SetFetchInterval(
class FetchInterval(
private val getChapterByMangaId: GetChapterByMangaId,
) {
@@ -27,7 +25,10 @@ class SetFetchInterval(
window
}
val chapters = getChapterByMangaId.await(manga.id)
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters,
dateTime.zone,
)
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
@@ -39,31 +40,34 @@ class SetFetchInterval(
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 = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
val lowerBound = today.minusDays(GRACE_PERIOD)
val upperBound = today.plusDays(GRACE_PERIOD)
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
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
val uploadDates = chapters.asSequence()
.filter { it.dateUpload > 0L }
.sortedByDescending { it.dateUpload }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
.toLocalDate()
.atStartOfDay()
}
.distinct()
val fetchDates = sortedChapters
.take(10)
.toList()
val fetchDates = chapters.asSequence()
.sortedByDescending { it.dateFetch }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
.toLocalDate()
.atStartOfDay()
}
.distinct()
.take(10)
.toList()
val interval = when {
// Enough upload date from source
@@ -82,7 +86,7 @@ class SetFetchInterval(
else -> 7
}
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
return interval.coerceIn(1, MAX_INTERVAL)
}
private fun calculateNextUpdate(
@@ -95,7 +99,10 @@ class SetFetchInterval(
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
manga.fetchInterval == 0
) {
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
val latestDate = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(manga.lastUpdate),
dateTime.zone,
)
.toLocalDate()
.atStartOfDay()
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
@@ -110,7 +117,7 @@ class SetFetchInterval(
}
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
// double delta again if missed more than 9 check in new delta
val cycle = timeSinceLatest.floorDiv(delta) + 1
@@ -120,4 +127,10 @@ class SetFetchInterval(
delta
}
}
companion object {
const val MAX_INTERVAL = 28
private const val GRACE_PERIOD = 1L
}
}

View File

@@ -20,7 +20,10 @@ class GetApplicationRelease(
val now = Instant.now()
// Limit checks to once every 3 days at most
if (arguments.forceCheck.not() && now.isBefore(Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS))) {
if (arguments.forceCheck.not() && now.isBefore(
Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS),
)
) {
return Result.NoNewUpdate
}
@@ -29,7 +32,12 @@ class GetApplicationRelease(
lastChecked.set(now.toEpochMilli())
// Check if latest version is different from current version
val isNewVersion = isNewVersion(arguments.isPreview, arguments.commitCount, arguments.versionName, release.version)
val isNewVersion = isNewVersion(
arguments.isPreview,
arguments.commitCount,
arguments.versionName,
release.version,
)
return when {
isNewVersion && arguments.isThirdParty -> Result.ThirdPartyInstallation
isNewVersion -> Result.NewUpdate(release)
@@ -37,7 +45,12 @@ class GetApplicationRelease(
}
}
private fun isNewVersion(isPreview: Boolean, commitCount: Int, versionName: String, versionTag: String): Boolean {
private fun isNewVersion(
isPreview: Boolean,
commitCount: Int,
versionName: String,
versionTag: String,
): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (isPreview) {

View File

@@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
class StubSource(
override val id: Long,
@@ -14,36 +13,16 @@ class StubSource(
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
override suspend fun getMangaDetails(manga: SManga): SManga {
override suspend fun getMangaDetails(manga: SManga): SManga =
throw SourceNotInstalledException()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(SourceNotInstalledException())
}
override suspend fun getChapterList(manga: SManga): List<SChapter> {
override suspend fun getChapterList(manga: SManga): List<SChapter> =
throw SourceNotInstalledException()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(SourceNotInstalledException())
}
override suspend fun getPageList(chapter: SChapter): List<Page> {
override suspend fun getPageList(chapter: SChapter): List<Page> =
throw SourceNotInstalledException()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(SourceNotInstalledException())
}
override fun toString(): String {
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
}
override fun toString(): String =
if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
}
class SourceNotInstalledException : Exception()

View File

@@ -10,7 +10,7 @@ class GetUpdates(
) {
suspend fun await(read: Boolean, after: Long): List<UpdatesWithRelations> {
return repository.awaitWithRead(read, after)
return repository.awaitWithRead(read, after, limit = 500)
}
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> {
@@ -18,6 +18,6 @@ class GetUpdates(
}
fun subscribe(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeWithRead(read, after)
return repository.subscribeWithRead(read, after, limit = 500)
}
}

View File

@@ -5,9 +5,9 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
interface UpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long): List<UpdatesWithRelations>
suspend fun awaitWithRead(read: Boolean, after: Long, limit: Long): List<UpdatesWithRelations>
fun subscribeAll(after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long, limit: Long): Flow<List<UpdatesWithRelations>>
}

View File

@@ -34,7 +34,10 @@ class LibraryFlagsTest {
@Test
fun `Test Flag plus operator with old flag as base`() {
val currentSort = LibrarySort(LibrarySort.Type.UnreadCount, LibrarySort.Direction.Descending)
val currentSort = LibrarySort(
LibrarySort.Type.UnreadCount,
LibrarySort.Direction.Descending,
)
currentSort.flag shouldBe 0b00001100
val sort = LibrarySort(LibrarySort.Type.DateAdded, LibrarySort.Direction.Ascending)

View File

@@ -0,0 +1,127 @@
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.ZoneOffset
import java.time.ZonedDateTime
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.toJavaDuration
@Execution(ExecutionMode.CONCURRENT)
class FetchIntervalTest {
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
private val testZoneId = ZoneOffset.UTC
private var chapter = Chapter.create().copy(
dateFetch = testTime.toEpochSecond() * 1000,
dateUpload = testTime.toEpochSecond() * 1000,
)
private val fetchInterval = FetchInterval(mockk())
@Test
fun `returns default interval of 7 days when not enough distinct days`() {
val chaptersWithUploadDate = (1..50).map {
chapterWithTime(chapter, 1.days)
}
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
it.copy(dateUpload = 0L)
}
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7
}
@Test
fun `returns interval based on more recent chapters`() {
val oldChapters = (1..5).map {
chapterWithTime(chapter, (it * 7).days) // Would have interval of 7 days
}
val newChapters = (1..10).map {
chapterWithTime(chapter, oldChapters.lastUploadDate() + it.days)
}
val chapters = oldChapters + newChapters
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
}
@Test
fun `returns interval of 7 days when multiple chapters in 1 day`() {
val chapters = (1..10).map {
chapterWithTime(chapter, 10.hours)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
}
@Test
fun `returns interval of 7 days when multiple chapters in 2 days`() {
val chapters = (1..2).map {
chapterWithTime(chapter, 1.days)
} + (1..5).map {
chapterWithTime(chapter, 2.days)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
}
@Test
fun `returns interval of 1 day when chapters are released every 1 day`() {
val chapters = (1..20).map {
chapterWithTime(chapter, it.days)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
}
@Test
fun `returns interval of 1 day when delta is less than 1 day`() {
val chapters = (1..20).map {
chapterWithTime(chapter, (15 * it).hours)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
}
@Test
fun `returns interval of 2 days when chapters are released every 2 days`() {
val chapters = (1..20).map {
chapterWithTime(chapter, (2 * it).days)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2
}
@Test
fun `returns interval with floored value when interval is decimal`() {
val chaptersWithUploadDate = (1..5).map {
chapterWithTime(chapter, (25 * it).hours)
}
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
it.copy(dateUpload = 0L)
}
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1
}
@Test
fun `returns interval of 1 day when chapters are released just below every 2 days`() {
val chapters = (1..20).map {
chapterWithTime(chapter, (43 * it).hours)
}
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
}
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
private fun List<Chapter>.lastUploadDate() =
last().dateUpload.toDuration(DurationUnit.MILLISECONDS)
}

View File

@@ -1,104 +0,0 @@
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.ZonedDateTime
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.toJavaDuration
@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 = (1..2).map {
chapterWithTime(chapter, 10.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
val chapters = (1..5).map {
chapterWithTime(chapter, 10.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
val chapters = (1..2).map {
chapterWithTime(chapter, 24.hours)
} + (1..5).map {
chapterWithTime(chapter, 48.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns default of 1 day when interval less than 1`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (15 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
// Normal interval calculation
@Test
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (24 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (48 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
}
@Test
fun `calculateInterval returns floored value when interval is decimal`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (25 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (43 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
val chapters = (1..5).map {
chapterWithTime(chapter, (25 * it).hours).copy(dateUpload = 0L)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
}

View File

@@ -79,7 +79,9 @@ class GetApplicationReleaseTest {
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(
release,
).release
}
@Test
@@ -106,7 +108,9 @@ class GetApplicationReleaseTest {
),
)
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(release).release
(result as GetApplicationRelease.Result.NewUpdate).release shouldBe GetApplicationRelease.Result.NewUpdate(
release,
).release
}
@Test