# NuvioTV vs UNSPOOLED — Architecture Comparison

Last updated: 2026-05-28

## Scale
| Metric | NuvioTV | UNSPOOLED |
|---|---|---|
| Kotlin files (main) | 461 | 266 |
| UI screens | 184 | 87 |
| Settings screens | 36 | 15 |
| Domain models | 32 | 21 |
| DI modules | 5 | 3 |
| Test files | ~7 | 7 |
| Tests passing | unknown | 131 |

## BuildConfig API Keys

All values read from gradle props or env vars. Empty strings are valid in debug builds.

### NuvioTV keys (from build.gradle.kts)
- TMDB_API_KEY, TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET
- TRAKT_REDIRECT_URI (default: urn:ietf:wg:oauth:2.0:oob)
- TRAKT_API_URL (default: https://api.trakt.tv/)
- PREMIUMIZE_CLIENT_ID
- SUPABASE_URL, SUPABASE_ANON_KEY
- PARENTAL_GUIDE_API_URL, INTRODB_API_URL, TRAILER_API_URL
- IMDB_RATINGS_API_BASE_URL, IMDB_TAPFRAME_API_BASE_URL
- TV_LOGIN_WEB_BASE_URL, DONATIONS_BASE_URL, DONATIONS_DONATE_URL
- AVATAR_PUBLIC_BASE_URL, UNIQUE_CONTRIBUTIONS_BASE_URL

### UNSPOOLED keys (from build.gradle.kts)
- TMDB_API_KEY, TRAKT_CLIENT_ID, TRAKT_CLIENT_SECRET
- TRAKT_REDIRECT_URI (default: urn:ietf:wg:oauth:2.0:oob)
- TRAKT_API_URL (default: https://api.trakt.tv/)
- PREMIUMIZE_CLIENT_ID
- RD_CLIENT_ID
- NEXSTREAM_CATALOG_BASE_URL

## Features UNSPOOLED Has That NuvioTV Doesn't
- **ProviderRegistry** — modular, multi-provider stream sources (6 files)
- **3-tier catalog fallback** — worker → TMDB → Stremio addons
- **DebridEnricher** — enriches all streams with cache status
- **SourceRanker** — 3 presets, device capabilities, HDR/audio scoring
- **StreamValidator** — URL validation with fallback retry
- **FallbackEngine** — retry logic for transient failures
- **SecureTokenStore** — Keystore AES-256-GCM for all secrets
- **SensitiveLogSanitizer** — token regex in log output
- **Addon health monitor** — real network checks
- **Torrentio multi-provider** — builds URL from ALL active debrid tokens
- **Manifest cache** — disk-persisted with TTL
- **Kids profile filtering** — ContentFilter with genre/rating safety

## Features NuvioTV Has That UNSPOOLED Deliberately Doesn't
- **Sync orchestrator** (16 files) — replaced by NexStream catalog worker
- **Cloud backend/Supabase** (2 files) — NexStream has own backend
- **Repository layer** (24 files) — inlined in data/* for simplicity
- **QR server pairing** (9 files) — simpler addon config server instead
- **In-app updates** — not yet needed

## Force-Unwrap Elimination Guide

### Detection
```bash
# CORRECT - catches all patterns
grep -rn '!!' --include='*.kt' app/src/main/

# WRONG - misses !!), !!, etc
grep -rn '!!\.' --include='*.kt' app/src/main/
```

### Common patterns and fixes

**Pattern 1: if-null-check + !!**
```kotlin
// BEFORE
if (x != null) { doSomething(x!!) }

// AFTER — local val enables smart-cast for var properties
val local = x
if (local != null) { doSomething(local) }
```
WARNING: `?.let { }` breaks `else` branches. Use local val + if/else.

**Pattern 2: isNullOrBlank + !!**
```kotlin
// BEFORE — isNullOrBlank doesn't enable smart-cast
!stream.url.isNullOrBlank() -> stream.url!!

// AFTER — requireNotNull returns non-null type
!stream.url.isNullOrBlank() -> requireNotNull(stream.url)
```

**Pattern 3: Map access + !!**
```kotlin
// BEFORE
states[key] = states[key]!!.copy(...)

// AFTER — use ?: return@update state in StateFlow.update blocks
val current = states[key] ?: return@update state
states[key] = current.copy(...)
```

**Pattern 4: Scope return labels**
When extracting null checks inside coroutine lambdas, verify the enclosing label:
```kotlin
// Inside viewModelScope.launch { ... }
val link = nullable ?: return@launch  // NOT return@withContext
```
