# UNSPOOLED — Project State (2026-05-29)

**Single source of truth.** Read this first in any new session.

## Quick Start

```bash
cd /home/rurouni/nexstream
./gradlew :app:assembleDebug       # APK at app/build/outputs/apk/debug/app-debug.apk (~31MB)
./gradlew :app:testDebugUnitTest   # 131 tests (6 SourceRanker tests currently failing — known bug)
```

- **Source:** 278 Kotlin files (5 dead files removed May 29)
- **Target:** Android TV API 23+, Jetpack Compose TV rc01, Hilt DI, Media3/ExoPlayer
- **App:** UNSPOOLED (`com.nexstream.app`)
- **Theme:** NuvioTheme (7 presets), pure black backgrounds, blue accents ~#00A3FF

---

## Architecture

```
app/src/main/java/com/nexstream/app/
├── core/               # androidtv/ (TV channels), network/ (IPv4FirstDns), profile/ (ProfileManager)
├── data/
│   ├── addon/          # Stremio client, health monitor, marketplace (22 presets), cache (6h TTL)
│   │   └── server/     # AddonConfigServer (NanoHTTPD QR-code phone pairing)
│   │   └── provider/   # AIOStreams, MediaFusion, Torrentio + ProviderRegistry
│   ├── backup/         # Backup/restore
│   ├── catalog/        # NexStreamCatalogApi (Cloudflare Worker backend)
│   ├── debrid/         # RD/AllDebrid/Premiumize/TorBox clients + DebridService + DebridAuthManager
│   ├── di/             # Hilt modules (network timeouts, log sanitiser, qualifiers)
│   ├── justwatch/      # JustWatch availability API
│   ├── local/          # Room DB v6, DataStore, SecureTokenStore (Keystore AES-256/GCM)
│   ├── recommendations/ # Android TV recommendation channel
│   ├── remote/         # API DTOs
│   ├── repository/     # Repositories (Addon, Catalog, etc.)
│   ├── source/         # SourceRanker, SourcePreferences, FallbackEngine, StreamDeduplicator, StreamFilterEngine
│   ├── subtitle/       # OpenSubtitles integration
│   ├── tmdb/           # TMDB API + repository + genre cache + parallel enrichment (CompletableDeferred dedup)
│   ├── trailer/        # InAppYouTubeExtractor (InnerTube reverse-engineered)
│   ├── trakt/          # TraktAuthManager (circuit breaker), TraktSyncManager, TraktScrobbler
│   └── watched/        # Watched content tracking
├── domain/
│   ├── model/          # Core models (Stream, MetaPreview, MetaItem, PlaybackState, etc.)
│   ├── playback/       # PlaybackOrchestrator, StreamValidator, DebridResolveCache
│   ├── usecase/        # 6 use cases (playback, addons, watchlist, continue watching, debrid auth, profile)
│   └── util/           # ContentFilter
├── player/             # PlayerViewModel, PlayerManager (ExoPlayer), PlaybackSpeedAwareAudioSink + GainAudioProcessor
├── ui/
│   ├── components/     # Shared composables (MediaCard, FocusScalingCard, StatsOverlay)
│   │   └── netflix/    # Netflix-style: GlassDockSidebar, NetflixSideNavigation, ContinueWatchingCard, HeroSection
│   ├── navigation/     # NexStreamNavHost (31 routes), Screen sealed class
│   ├── screens/        # 27 screen packages (home, search, details, player, addons, profiles, settings×8, etc.)
│   ├── theme/          # NuvioTheme (7 presets)
│   └── util/           # D-pad utilities, blur, marquee, long-press tracker
└── util/               # CrashLogger (dedicated CoroutineScope, rolling 256KB log)
```

---

## Playback Pipeline

```
User selects content
  → AddonRepository.getStreamsForContent() (all enabled addons, parallel)
  → StreamDeduplicator.deduplicate() (union-find + SmartDetect)
  → SourceRanker.rankStreams() (3 presets: Recommended/Quality First/Speed First)
  → PlaybackOrchestrator.prepare() (StreamValidator + DebridResolveCache)
  → PlayerViewModel.playStream() (ExoPlayer with resume position)
  → PlayerScreen renders video + controls + overlays
```

---

## Key Systems

- **Debrid:** RD (magnet→torrent lifecycle 120s), AllDebrid/Premiumize/TorBox via adapter; device-code OAuth; SecureTokenStore AES-256/GCM
- **Trakt:** Circuit breaker (5→60min cooldown), rate limiter (900/5min), Mutex token refresh, device-code OAuth, scrobbling + watchlist sync
- **Profiles:** ProfileManager (activeProfileId StateFlow, per-profile DataStore, CRUD, max 5 profiles, auto-switch on delete)
- **Navigation:** CircularRailNavigation (experimental curved rail, auto-collapse 3s, expand/collapse on D-pad) + NavigationStyleSettingsScreen selector
- **Audio:** PlaybackSpeedAwareAudioSink (DefaultRenderersFactory override), GainAudioProcessor (amplification + playback speed in MoreMenu)

---

## Security Posture

- **Network:** HTTPS-by-default; cleartext only for localhost + debrid OAuth domains
- **Permissions:** INTERNET + ACCESS_NETWORK_STATE only
- **Token storage:** Keystore-backed AES-256/GCM
- **API keys:** BuildConfig from gradle.properties/env; empty values → clear runtime error
- **Logging:** Zero secrets (audited); SensitiveLogSanitizer on all OkHttp interceptors
- **ProGuard:** isMinifyEnabled=true, isShrinkResources=true, full keep rules
- **Release:** Conditional signing via RELEASE_STORE_FILE Gradle property

---

## Conventions

- No hardcoded secrets — all API keys via BuildConfig/gradle.properties
- D-pad first — `onPreviewKeyEvent` everywhere; `Key.DpadCenter || Key.Enter` (NOT `Key.DirectionCenter`)
- Theme — `NuvioTheme.colors.X` exclusively
- DI — `@HiltViewModel` for VMs, `@Singleton @Inject` for services
- Architecture — composables → ViewModels → domain use cases → data repositories
- Build — `./gradlew :app:assembleDebug` must be green before commit

---

*Last updated: 2026-05-29. 278 KT files. Build clean. 131 tests (6 SourceRanker failures pending).*