# Phase 1 Stream Filter Architecture (2026-05-22)

## What Was Built
Three features copied from AIOStreams, adapted for Android TV/Kotlin:
1. **Regex stream filters** — per-content-type regex rules applied in stream pipeline
2. **Per-content-type filter presets** — separate rules for movies/series/anime
3. **Addon groups with early exit** — sequential group execution with threshold-based short-circuit

## Architecture

### StreamFilterEngine (already existed, newly wired)
File: `data/source/StreamFilterEngine.kt`

3-pass filter pipeline:
- **Pass 1 (REQUIRE)**: Drops everything that doesn't match REQUIRE rules
- **Pass 2 (EXCLUDE)**: Removes streams matching EXCLUDE rules
- **Pass 3 (INCLUDE)**: Marks preferred streams for ranking bonus (no filtering)

**FilterTarget enum:**
- FILENAME — matches against `stream.name + title + description`
- ADDON — matches against `stream.addonName`
- RELEASE_GROUP — extracts group via regex `[-]([A-Za-z0-9]{2,20})$`

**Fail-safe:** Invalid regex patterns are silently skipped (logged). Filter exceptions return unfiltered streams.

### PreferencesManager Storage
```
stringSetPreferencesKey("regex_filter_rules")
// Each entry: "<json_rules>::type=<contentType>"
// Content types: "movie", "series", "anime"

stringPreferencesKey("addon_groups")
// JSON array of AddonGroupConfig objects
```

### AddonRepository Wiring
**Constructor:** Now injects `PreferencesManager`, creates `StreamFilterEngine` instance.

**getStreamsForContent(type, id):** Fetches raw streams → applies regex filters for content type → returns filtered result.

**getStreamsForContentGrouped(type, id, groups):** Delegates to `StremioAddonClientImpl.getStreamsGrouped()` → applies regex filters.

**applyFilters(streams, contentType):** Private helper — reads rules from PreferencesManager, runs StreamFilterEngine.apply(), logs exclusion count.

### StremioAddonClientImpl.getStreamsGrouped()
New method on the implementation class (not on the interface):
```kotlin
suspend fun getStreamsGrouped(
    allAddons: List<AddonInstance>,
    groups: List<AddonGroupConfig>,
    type: String, id: String,
): GroupedStreamResult
```

**Algorithm:**
1. Map addon IDs to instances
2. Filter enabled groups, sort by order
3. For each group: collect addons → call `getStreams()` (parallel within group) → aggregate results
4. If `group.earlyExitThreshold > 0 && allStreams.size >= threshold`: break
5. Final deduplication pass on all collected streams

### AddonGroupConfig Model
File: `domain/model/AddonGroup.kt`
```kotlin
data class AddonGroupConfig(
    id: String, name: String, addonIds: List<String>,
    earlyExitThreshold: Int, enabled: Boolean, order: Int,
)
```

### StreamFilterViewModel
File: `ui/screens/settings/StreamFilterViewModel.kt`
- Loads rules per content type from PreferencesManager
- CRUD operations: addRule(), removeRule(), toggleRule(), updateRule()
- Auto-persists on every change
- Validates regex via `Regex()` constructor (fail-safe)

### StreamFilterScreen (UI)
File: `ui/screens/settings/StreamFilterScreen.kt`
- Content type tabs (Movies / Series / Anime)
- Add Rule dialog: regex pattern, label, mode, target selectors
- Rule cards: mode badge (red/green/yellow), target badge, monospace pattern, toggle, delete
- Empty state with help text

### Navigation
- `Screen.SettingsFilter` route ("settings/filters")
- Composables in NexStreamNavHost
- SettingsScreen card: "Stream Filters" with description
- SettingsScreen card: "Addon Groups" (placeholder, no UI yet)

## Key Pitfalls Discovered

### 1. Don't cast interface to implementation without checking
`client as? StremioAddonClientImpl` — safe cast with null fallback. If Hilt provides a different implementation, falls back to standard parallel fetch.

### 2. PreferencesManager stringSet key encoding
The `::type=` delimiter pattern separates JSON rules from content type in stored set entries. Must handle removal of old entries before inserting new ones.

### 3. Filter failures must be fail-open
If StreamFilterEngine or JSON deserialization throws, return unfiltered streams. Never let a broken regex block all content.

### 4. AddonGroup store is a single JSON string
Storing as `stringPreferencesKey` (not `stringSetPreferencesKey`) because the groups array is a single atomic config — no per-content-type separation needed (groups are global).

## Build Verification
```bash
cd /home/rurouni/nexstream
./gradlew compileDebugKotlin  # 0 errors
./gradlew test                 # 17/17 pass
```

## Commit
`7ec683a` — 10 files changed, +1,056 lines
