fix(eta): tame reading-time estimates (idle + cumulative history)
- Cap per-chapter samples used for averages at 45m (stats only). - Prefer session ms/page extrapolation for current chapter so cumulative DB read time over revisits no longer blows up duration/progress. - Floor uncertain page rates using conservative ms/page until enough samples. - Shared defaults in ReadingEtaDefaults for reader + library badge. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9ce3f0ba29
commit
07019175d1
3 changed files with 62 additions and 6 deletions
|
|
@ -106,6 +106,7 @@ import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.library.model.LibrarySort
|
import tachiyomi.domain.library.model.LibrarySort
|
||||||
import tachiyomi.domain.library.model.sort
|
import tachiyomi.domain.library.model.sort
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.reading.ReadingEtaDefaults
|
||||||
import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
|
import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
|
||||||
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
||||||
|
|
@ -483,6 +484,7 @@ class LibraryScreenModel(
|
||||||
): Long? {
|
): Long? {
|
||||||
val sampledDurations = chapterDurations
|
val sampledDurations = chapterDurations
|
||||||
.filter { it in MIN_TRACKED_CHAPTER_DURATION_MS..MAX_TRACKED_CHAPTER_DURATION_MS }
|
.filter { it in MIN_TRACKED_CHAPTER_DURATION_MS..MAX_TRACKED_CHAPTER_DURATION_MS }
|
||||||
|
.map { it.coerceAtMost(ReadingEtaDefaults.MAX_CHAPTER_DURATION_MS_FOR_STATS) }
|
||||||
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
||||||
if (sampledDurations.isEmpty()) return null
|
if (sampledDurations.isEmpty()) return null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||||
import tachiyomi.domain.history.model.HistoryUpdate
|
import tachiyomi.domain.history.model.HistoryUpdate
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.reading.ReadingEtaDefaults
|
||||||
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
import tachiyomi.domain.manga.interactor.GetFlatMetadataById
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
import tachiyomi.domain.manga.interactor.GetMergedMangaById
|
||||||
|
|
@ -1001,10 +1002,13 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
currentPage = state.value.currentPage,
|
currentPage = state.value.currentPage,
|
||||||
totalPages = state.value.totalPages,
|
totalPages = state.value.totalPages,
|
||||||
)
|
)
|
||||||
val projectedCurrentChapterDuration = calculateProjectedChapterDurationMs(
|
val projectedCurrentChapterDuration =
|
||||||
chapterDuration = chapterDurations[currentChapterId] ?: 0L,
|
estimateFullChapterMsFromActiveSession()
|
||||||
chapterProgress = currentChapterProgress,
|
?.coerceAtMost(ReadingEtaDefaults.MAX_CHAPTER_DURATION_MS_FOR_STATS)
|
||||||
)
|
?: calculateProjectedChapterDurationMs(
|
||||||
|
chapterDuration = chapterDurations[currentChapterId] ?: 0L,
|
||||||
|
chapterProgress = currentChapterProgress,
|
||||||
|
)
|
||||||
val averageChapterDuration = calculateAverageChapterDurationMs(
|
val averageChapterDuration = calculateAverageChapterDurationMs(
|
||||||
chapterDurations = chapterDurations,
|
chapterDurations = chapterDurations,
|
||||||
currentChapterId = currentChapterId,
|
currentChapterId = currentChapterId,
|
||||||
|
|
@ -1131,7 +1135,10 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
||||||
if (pageRates.isEmpty()) return null
|
if (pageRates.isEmpty()) return null
|
||||||
val avgMsPerPage = pageRates.average().toLong().coerceAtLeast(1L)
|
var avgMsPerPage = pageRates.average().toLong().coerceAtLeast(1L)
|
||||||
|
if (pageRates.size < ReadingEtaDefaults.MIN_TRUSTED_PAGE_RATE_SAMPLES) {
|
||||||
|
avgMsPerPage = maxOf(avgMsPerPage, ReadingEtaDefaults.FALLBACK_MS_PER_PAGE)
|
||||||
|
}
|
||||||
|
|
||||||
val sampledPages = orderedSeriesChapterIds
|
val sampledPages = orderedSeriesChapterIds
|
||||||
.take(currentChapterIndex + 1)
|
.take(currentChapterIndex + 1)
|
||||||
|
|
@ -1176,6 +1183,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
normalizedDuration
|
normalizedDuration
|
||||||
}
|
}
|
||||||
.filter { it in MIN_TRACKED_CHAPTER_DURATION_MS..MAX_TRACKED_CHAPTER_DURATION_MS }
|
.filter { it in MIN_TRACKED_CHAPTER_DURATION_MS..MAX_TRACKED_CHAPTER_DURATION_MS }
|
||||||
|
.map { it.coerceAtMost(ReadingEtaDefaults.MAX_CHAPTER_DURATION_MS_FOR_STATS) }
|
||||||
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
.takeLast(ETA_SAMPLE_CHAPTER_LIMIT)
|
||||||
if (sampledDurations.isEmpty()) return null
|
if (sampledDurations.isEmpty()) return null
|
||||||
|
|
||||||
|
|
@ -1208,7 +1216,30 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
?.coerceIn(0.0, 1.0)
|
?.coerceIn(0.0, 1.0)
|
||||||
?: return null
|
?: return null
|
||||||
if (progress == 0.0) return null
|
if (progress == 0.0) return null
|
||||||
return (chapterDuration.toDouble() / progress).toLong().coerceAtLeast(0L)
|
return (chapterDuration.toDouble() / progress).toLong()
|
||||||
|
.coerceAtLeast(0L)
|
||||||
|
.coerceAtMost(ReadingEtaDefaults.MAX_CHAPTER_DURATION_MS_FOR_STATS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates full-chapter duration from the current session's ms/page rate.
|
||||||
|
* Prefer over scaling cumulative DB read time by absolute chapter progress — history times are
|
||||||
|
* summed across revisits, which inflates `duration / progress` for rereads.
|
||||||
|
*/
|
||||||
|
private fun estimateFullChapterMsFromActiveSession(): Long? {
|
||||||
|
val totalPages = state.value.totalPages
|
||||||
|
val startPageIndex = chapterReadStartPageIndex ?: return null
|
||||||
|
val startTime = chapterReadStartTime ?: return null
|
||||||
|
if (totalPages <= 1) return null
|
||||||
|
|
||||||
|
val sessionMs = (SystemClock.elapsedRealtime() - startTime).sanitizeTrackedDuration()
|
||||||
|
if (sessionMs < MIN_TRACKED_CHAPTER_DURATION_MS) return null
|
||||||
|
|
||||||
|
val currentPageIndex = (state.value.currentPage - 1).coerceAtLeast(startPageIndex)
|
||||||
|
val pagesThisSession = (currentPageIndex - startPageIndex + 1).coerceAtLeast(1)
|
||||||
|
val msPerPage = sessionMs.toDouble() / pagesThisSession.toDouble()
|
||||||
|
val estimated = (msPerPage * totalPages.toDouble()).toLong()
|
||||||
|
return estimated.takeIf { it > 0L }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateRemainingChapterTimeMs(
|
private fun calculateRemainingChapterTimeMs(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package tachiyomi.domain.reading
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared defaults for "time left" / reading-speed estimates in the library and reader.
|
||||||
|
* These cap idle-inflated history and avoid ultra-optimistic page rates from skimming
|
||||||
|
* the first few pages.
|
||||||
|
*/
|
||||||
|
object ReadingEtaDefaults {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When building per-chapter averages, treat each sample as at most this long.
|
||||||
|
* Stops a single tab-left-open session from blowing up series-wide ETAs.
|
||||||
|
*/
|
||||||
|
const val MAX_CHAPTER_DURATION_MS_FOR_STATS: Long = 45 * 60 * 1000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we have few per-chapter page-rate samples, do not go below this ms/page
|
||||||
|
* (avoids "2 minutes for the whole series" after a quick flip through ch.1).
|
||||||
|
*/
|
||||||
|
const val FALLBACK_MS_PER_PAGE: Long = 12_000L
|
||||||
|
|
||||||
|
const val MIN_TRUSTED_PAGE_RATE_SAMPLES: Int = 3
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue