# Source Ranking Design — 2026-05-25 Rewrite

## Overview

The `SourceRanker` system scores and sorts available streams for playback. Three user-selectable presets control behavior. As of the 2026-05-25 rewrite, the default changed from "Balanced" to **"Recommended"**, reflecting the principle that the default experience should prioritize instant, stable playback at good-enough quality — not the largest/best file.

## Presets

### Recommended (default, was "Balanced")

**Tagline:** "Play Now = fast, stable, good enough quality."

Cached 1080p HEVC 3–8 GB is the ideal target. Uncached 4K REMUX is heavily penalized (slow startup for average user). Unsupported codecs (AV1, Dolby Vision on non-DV displays) are down-ranked.

Scoring adjustments:
- Resolution: cached 4K +50, uncached 4K −300, 1080p +200 (sweet spot), 720p +100, SD −50
- REMUX: cached+capable HW +50, cached+weak HW −30, uncached −250
- DV: −200 on non-DV device, 0 on DV device
- AV1 without HW decode: −250
- Uncached 4K: −250 (separate from resolution)
- Files >25 GB: −100 (even when cached)
- Base multipliers: reliability ×1.0, quality ×0.7, size ×1.0

### Quality First (power users)

**Tagline:** "Prioritizes 4K, HDR, and larger files when supported."

Cached 4K REMUX boosted +200. DV on DV device +150. Still penalizes uncached sources and unplayable codecs.

Scoring adjustments:
- Resolution: cached 4K +300, uncached 4K +50, 1080p +150
- REMUX: cached +200, uncached −100
- DV: DV device +150, non-DV device −250, HDR10 device +80
- AV1 without HW: −300
- Uncached 4K: −150 (less severe than Recommended)
- Base multipliers: reliability ×0.9, quality ×1.4, size ×0.6

### Speed First (weak connections, was "Cache First")

**Tagline:** "Prioritizes cached, smaller, faster-starting sources."

Cached-only wins. 720p–1080p sweet spot. 4K severely penalized. H.264 preferred over HEVC on low-power devices.

Scoring adjustments:
- Resolution: 4K −400, 1080p +150, 720p +200
- REMUX: −300
- Uncached: −350
- Files >10 GB: −200
- H.264 on low-power: +80
- Base multipliers: reliability ×2.0 (cached is king), quality ×0.4, size ×1.8

## Device Capability Detection

`DeviceCapabilitiesProvider` auto-detects via Android's `MediaCodecList`:
- HEVC hardware decode
- AV1 hardware decode
- Dolby Vision support
- HDR10 support
- Android TV vs phone/tablet
- Low-RAM/low-power device (Android Go, TV sticks)

If detection fails, `DeviceCapabilities.UNKNOWN` is a conservative fallback that assumes nothing is supported.

## File Size Scoring (Episode vs Movie)

The `sizeScore()` function adjusts ideal ranges based on content type:
- **Movie:** ideal 3–8 GB. Scores: 3-8=1.0, 0.3-3=0.85, 8-16=0.7, 16-30=0.3, >30=0.1
- **Episode:** ideal 0.8–3 GB. Same relative scoring with lower thresholds.

## Migration

Legacy DataStore values are migrated automatically via `resolveSortPreset()`:

| Old stored value | Maps to |
|-----------------|---------|
| `"balanced"` | `RECOMMENDED` |
| `"cache_first"` | `SPEED_FIRST` |
| `"quality_first"` | `QUALITY_FIRST` |

Old enum names (`BALANCED`, `CACHE_FIRST`) are `@Deprecated` but kept for binary compatibility. They normalize to their replacements at the top of `rankStreams()`.

## File Map

| File | Role |
|------|------|
| `data/source/SourceRanker.kt` | Main ranking engine, presets, adjustments |
| `data/source/DeviceCapabilities.kt` | HW codec/HDR detection |
| `data/local/PreferencesManager.kt` | Stores `sort_preset` (default: `"recommended"`) |
| `ui/screens/settings/SettingsPlaybackScreen.kt` | Chip selector: Recommended / Quality First / Speed First |
| `ui/screens/details/DetailsViewModel.kt` | Passes device caps + isEpisode to ranker |
| `player/PlayerViewModel.kt` | Same for player |
| `ui/screens/settings/SettingsViewModel.kt` | Holds sortPreset state |

## hdrScore Normalization

The `hdrScore()` function normalizes dots/underscores/hyphens to spaces before matching. Without this, "Dolby.Vision" (dot-separated, common in scene release names) wouldn't match "DOLBY VISION" (space-separated). This bug was discovered when tests with dot-separated titles failed.

## Kotlin Compilation Pitfalls

- **Missing import cascades:** A missing `import com.nexstream.app.domain.model.Stream` in SourceRanker.kt caused "Unresolved reference" errors in UNRELATED files (AllSourcesScreen.kt, DetailsViewModel.kt). The Kotlin compiler doesn't produce a clear chain — check ALL recent changes for missing imports when you see cascading failures.
- **When exhaustiveness with deprecated enums:** A `when` on a `SortPreset` value must handle the `@Deprecated` entries even if they're normalized earlier in the function. Add stub branches or the compiler rejects it.
- **Range destructuring:** Kotlin `ClosedFloatingPointRange` doesn't support destructuring (`val (lo, hi) = 0.8..3.0`). Use separate `.start`/`.endInclusive` properties or declare `lo`/`hi` as separate variables.
- **Numeric literal underscores:** `0_8` as a function argument may parse differently than expected on some Kotlin versions. Use simple integer literals (e.g., `1` instead of `0_8`).
