komikku/docs/reading-eta.md
littlecodedragon b299a9ce81 docs(reading-eta): document unified limits, long chapters, and settings
Capture library vs reader ETA behaviour, historical duplicate caps, tunable
preferences, and code map from the reading-eta work. Link from README.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 13:56:33 +02:00

135 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Reading time estimates (ETA)
Komikku shows estimated reading time in two places:
| Surface | What it shows |
|---------|----------------|
| **Library** | Time-left badge per entry (unread chapters × average chapter duration). |
| **Reader** | Series progress ETA (remaining time for the current series, including the active chapter). |
Both surfaces share the same **limits and defaults** via `ReadingEtaPreferences``ReadingEtaLimits`. Tunable in **Settings → Reader → Time estimate tuning** (`SettingsReaderScreen`, group `getReadingEtaTuningGroup`).
---
## Very long chapters (one entry = one huge chapter)
**Reading itself has no artificial page cap.** The reader uses the real `totalPages` from the loaded chapter. If a source exposes an entire volume as a single chapter, that is one DB chapter with many pages — normal behaviour.
**Limits apply only to statistics and ETA**, not to loading or turning pages:
- Sessions shorter than ~2 s or longer than the configured *max trusted session* are ignored for speed stats.
- When averaging read times, each chapter sample counts at most *max minutes per chapter sample* (default 45 min) so an idle tab does not inflate estimates.
- History rows with more than *max pages per history sample* (default 2000) are skipped for **page-rate** priors (cross-series hints and ms/page blending).
For **very long chapters**, the reader ETA path that scales by **remaining pages × ms/page** can still show a long ETA if your real pace is slow — that is intentional. If estimates look **too short**, raise the stats/session caps in settings (e.g. 180 min sample cap, 1440 min trusted session, 5000+ pages sample).
---
## Why there used to be two different logics
Before `f178a7919`, library and reader each duplicated constants in their ViewModels:
| Constant | Library (`LibraryScreenModel`) | Reader (`ReaderViewModel`) |
|----------|-------------------------------|----------------------------|
| Max trusted session | 1 h | 4 h |
| Chapters sampled | 60 | 20 |
Same *idea* (trim outliers, cap per-chapter duration for averages), but **different numbers** → inconsistent badges vs reader ETA on the same manga.
The reader additionally had a **page-rate path** (`ReadingEtaPageRateEstimator` + cross-manga history); the library used **chapter-duration averages only**. That asymmetry is by design (library has no “current page”), but the **shared caps** are now unified.
---
## Architecture (current)
```mermaid
flowchart TB
Prefs[ReadingEtaPreferences]
Limits[ReadingEtaLimits]
Lib[LibraryScreenModel]
Reader[ReaderViewModel]
Cross[GetCrossMangaPageRateHistorySamples]
Est[ReadingEtaPageRateEstimator]
Prefs --> Limits
Limits --> Lib
Limits --> Reader
Limits --> Cross
Cross --> Reader
Est --> Reader
Prefs -->|limitsFlow| Lib
```
### Key types and files
| Piece | Path |
|-------|------|
| Defaults (code constants) | `domain/.../reading/ReadingEtaDefaults.kt` |
| Effective limits snapshot | `domain/.../reading/ReadingEtaLimits.kt` |
| User preferences | `domain/.../reading/service/ReadingEtaPreferences.kt` |
| Page-rate blending | `domain/.../reading/ReadingEtaPageRateEstimator.kt` |
| Cross-series history samples | `domain/.../history/interactor/GetCrossMangaPageRateHistorySamples.kt` |
| Library ETA | `app/.../library/LibraryScreenModel.kt` |
| Reader series ETA | `app/.../reader/ReaderViewModel.kt` |
| Settings UI | `app/.../settings/screen/SettingsReaderScreen.kt` |
`ReadingEtaPreferences` is registered in `PreferenceModule`; `GetCrossMangaPageRateHistorySamples` takes it via Injekt (`DomainModule`).
---
## User settings
Preference keys (defaults in parentheses):
| Setting | Key | Default | Slider range (UI) |
|---------|-----|---------|-------------------|
| Max minutes per chapter sample | `pref_reading_eta_max_stats_min` | 45 | 5180 |
| Max trusted session length | `pref_reading_eta_max_trusted_session_min` | 240 (4 h) | 601440 |
| Chapters sampled for averages | `pref_reading_eta_sample_chapter_limit` | 60 | 5100 |
| Max pages per history sample | `pref_reading_eta_max_pages_sample` | 2000 | 100050000 (slider 10500 × 100) |
`ReadingEtaLimits.fromRaw` enforces:
- Trusted session ≥ stats cap (minutes).
- Sample chapter count ∈ [5, 200].
- Max pages ∈ [100, 50_000].
Strings: `i18n-kmk/.../base/strings.xml` and `de/strings.xml` (`pref_reading_eta_*`).
Reader-only toggles (unchanged): **Show series ETA**, **Persist series ETA** (`ReaderPreferences`).
---
## How estimates are computed
### Library badge
1. Load per-manga chapter read durations from history.
2. Filter to trusted session range; cap each sample at *max chapter duration for stats*.
3. Take the last *N* chapters (`etaSampleChapterLimit`), trim top/bottom 10% (when enough samples), average.
4. Multiply by unread count (or `1` when `totalChapters == 1`).
Falls back to a **global** average across the visible library when a title has no usable samples.
### Reader series ETA
Two paths; the UI uses **page-based ETA when available**, else **chapter-based**:
1. **Page-based** (`calculatePageBasedEtaMillis`): ms/page from recent chapters in this series + optional blend with cross-manga priors (`ReadingEtaPageRateEstimator`). Projects `remainingPages × ms/page` plus whole chapters ahead using average pages/chapter.
2. **Chapter-based**: average/projected chapter duration × remaining chapters, with projection from current session or progress.
Cross-manga priors: up to `CROSS_MANGA_PAGE_RATE_SAMPLE_LIMIT` (500) recent completed chapters from **other** series, preferring similar page counts (±15%, then ±30% band).
---
## Release
Shipped in tag **`v1.13.6-forgejo.38`** — commit `feat(reading-eta): unify library and reader limits; make caps tunable`.
---
## Maintainer notes
- When changing defaults, update `ReadingEtaDefaults`, `ReadingEtaPreferences` default getters, and the settings slider ranges if needed.
- Keep library and reader on `readingEtaPreferences.currentLimits()` / `limitsFlow()` — do not reintroduce local `MIN_TRACKED_*` constants in ViewModels.
- Page caps affect **stats only**; do not use them to limit `UpsertHistory` unless product intent changes.