# NexStream Catalog Pipeline Architecture

## Two-Source Catalog Strategy

NexStream loads home screen catalogs from **two independent sources**, each with its own fallback:

```
Priority rows (local + fast API)
  ├─ Continue Watching  → RoomDB WatchProgressDao
  ├─ Up Next            → TraktService.getTrending()
  ├─ Watchlist          → TraktService.getWatchlist()
  ├─ Trending Movies    → TMDB API → Stremio addons fallback
  └─ Trending Shows     → TMDB API → Stremio addons fallback

Catalog rows (TMDB definitions + addon-native)
  ├─ TmdbCatalogRepository.getHomeCatalogDefinitions()
  │   └─ 20+ hardcoded TmdbCatalogDefinition (popular, top, genres, OTT providers)
  │       └─ Each loaded via fetchCatalogSafe() → AddonRepository.getCatalogRows()
  │           └─ Tier 1: TMDB native → Tier 2: Worker relay → Tier 3: TMDB direct → Tier 4: Stremio addons
  └─ loadAddonCatalogRows() [FALLBACK — only if TMDB rows all empty]
      └─ Iterates addonRepository.getCatalogAddons() → manifest.catalogs[]
          └─ Each addon queried for its native catalog IDs (e.g., cinemeta.top)
```

## Key Files

| File | Role |
|------|------|
| `ui/screens/home/HomeViewModel.kt` | Orchestrates priority → catalog loading, fallback trigger |
| `ui/screens/home/NetflixHomeScreen.kt` | Renders hero + priority rows + catalog rows + catalogsUnavailable banner |
| `data/tmdb/TmdbCatalogRepository.kt` | Hardcoded TMDB catalog definitions, TMDB API calls |
| `data/tmdb/TmdbAuthInterceptor.kt` | Auto-detects v3 (api_key param) vs v4 (Bearer) key format |
| `data/tmdb/TmdbApiKeyHolder.kt` | Primary/fallback key cache with rotation on 401 |
| `data/tmdb/TmdbHttpErrorInterceptor.kt` | Records last TMDB error + triggers key rotation on auth failure |
| `data/addon/AddonRepository.kt` | 4-tier catalog fallback chain, addon manifest management |
| `data/addon/SystemAddonCatalog.kt` | Seeds: tmdb (native), cinemeta, tmdb_stremio, opensubtitles, etc. |
| `data/addon/UnifiedAddonStore.kt` | Room-backed addon persistence (manifest JSON, health, flags) |

## Fallback Chain (AddonRepository.getCatalogRows)

For every `catalogId` query:
1. **Tier 1**: `tmdbCatalog.getCatalogRow()` if ID starts with `tmdb.` — fails fast on 401
2. **Tier 1b**: `traktCatalog.getCatalogRow()` if `trakt.*` — separate API
3. **Tier 1c**: `aniListCatalog.getCatalogRow()` if `anilist.*` — separate API
4. **Tier 2**: `catalogRelay.getCatalogRow()` — Cloudflare Worker relay (skipped for native IDs)
5. **Tier 3**: `tmdbCatalog.getCatalogRow()` — TMDB direct fallback (redundant with Tier 1)
6. **Tier 4**: Stremio addons — queries each enabled catalog addon for the catalog ID

## HomeViewModel.loadHome() Flow

```
1. addonInit = async { addonRepository.initialize() }
2. Load profile ID + preferences
3. loadPriorityRows() — 5x async: ContinueWatching, UpNext, Watchlist, TrendingMovie, TrendingShow
4. hasApiKey() check — with try/catch
5. filterCatalogDefinitions() — filters by user prefs (Trakt rows, genre rows, OTT toggles)
6. Set UI state: priority rows + hero items + catalog placeholder rows
7. Load catalog rows in 2 batches (first 6, rest) with Semaphore(4)
8. await addonInit
9. If catalog rows all empty → loadAddonCatalogRows() fallback from Stremio addons
```

## Common Failure Modes

| Symptom | Root Cause | Fix |
|---------|-----------|-----|
| Empty home screen, no error | TMDB key is JWT sent as Bearer instead of v3 api_key param | Use v3 hex key or let TmdbAuthInterceptor detect format |
| "TMDB API key missing" | key in local.properties is blank or JWT format, hasApiKey() returns false | Set valid v3 hex key in local.properties |
| All catalog rows loading skeletons forever | TMDB returning 401 on every call, no fallback kicking in | Verify key with curl; ensure Stremio addon fallback is wired |
| Some rows empty, others work | Individual TMDB catalog IDs not mapped to addon catalogs | Cinemeta doesn't have `tmdb.trending_day` — uses `top` instead |
| Catalog rows show but are empty | Content filter (kids profile) eviscerated all items | Check ContentFilter.isAppropriateFor() logic |
| Build fails after adding TMDB_API_KEY_FALLBACK | buildConfigField variable scoping — need top-level val | Move to file scope before `android {` |

## Stremio Addon Fallback Architecture

When all TMDB catalog rows return empty (e.g., key invalid, API down):
1. `loadAddonCatalogRows()` is called
2. Gets `addonRepository.getCatalogAddons()` (enabled, healthy, has catalog resource)
3. Filters to addons with non-null manifests
4. For each addon (max 3), iterates manifest.catalogs[] (max 5 each)
5. Each catalog loaded via `loadCatalogRow()` → `fetchCatalogSafe()` → `AddonRepository.getCatalogRows()`
6. For addon-native IDs like `top` (not `tmdb.*`), Tier 1 skips TMDB, falls straight to Tier 4 (Stremio addons)
7. Results appended to existing catalog rows in UI state

## NuvioTV vs Nexstream Catalog Loading

| Aspect | NuvioTV | Nexstream |
|--------|---------|-----------|
| Primary source | Stremio addon manifests' `catalogs[]` | Hardcoded `TmdbCatalogDefinition` list |
| TMDB role | Metadata enrichment only (posters, ratings) | Primary catalog source |
| Addon role | ALL catalog rows come from addons | Fallback only (after TMDB fails) |
| Reactivity | `getInstalledAddons()` Flow → triggers full reload | One-time `addonRepository.initialize()` |
| Lazy loading | First 4 eager, rest on scroll with placeholders | All loaded in 2 batches |
| Concurrency | Semaphore(8) | Semaphore(4) |
| Per-row errors | Failed addon catalogs excluded from homeRows | `RowLoadResult.error` + retry button |

The TODO for matching NuvioTV: make addon-native catalogs PRIMARY rows on home screen (extract from manifests → show alongside TMDB rows). Currently they only appear as fallback when TMDB is completely empty.
