# TMDB API Key Debugging for Android TV Apps

## Key Format Detection

TMDB supports two auth formats. The `TmdbAuthInterceptor` auto-detects:
- **v3 API key** (32 hex chars, e.g. `ab74812bfa9665ebd944ffbbb5b57d76`) → `api_key` query parameter
- **v4 read access token** (JWT starting with `eyJ`) → `Authorization: Bearer`

## When Catalogs Are Empty

The #1 cause of empty catalogs is an invalid or misformatted TMDB key. Debug in order:

### 1. Verify the key works externally
```python
import urllib.request, json
key = "<key from local.properties>"

# Test v3 query param
url = f"https://api.themoviedb.org/3/trending/movie/day?language=en-US&api_key={key}"
try:
    resp = urllib.request.urlopen(urllib.request.Request(url), timeout=10)
    data = json.loads(resp.read())
    print(f"OK - {len(data['results'])} results")
except Exception as e:
    print(f"FAILED: {e}")

# Test v4 Bearer (only for JWT keys)
try:
    req = urllib.request.Request("https://api.themoviedb.org/4/list/1",
        headers={"Authorization": f"Bearer {key}"})
    resp = urllib.request.urlopen(req, timeout=10)
    print("v4 Bearer: OK")
except Exception as e:
    print(f"v4 Bearer FAILED: {e}")
```

### 2. If v3 key works with query param but app still empty
- Check `TmdbAuthInterceptor` is wired in the OkHttpClient
- Check `BuildConfig.TMDB_API_KEY` is generated from `local.properties` (rebuild after changing keys)
- Run `./gradlew clean :app:assembleDebug` to force BuildConfig regeneration
- Check `TmdbApiKeyProvider.resolve()` returns the correct key (reads DataStore → falls back to BuildConfig)

### 3. Common failure modes
| Symptom | Likely cause |
|---------|-------------|
| `status_code: 7, "Invalid API key"` | Key is wrong or expired |
| 401 with Bearer for v3 key | Key is v3 (hex) but sent as Bearer — interceptor should catch this |
| Empty catalogs but key works externally | `hasApiKey()` check blocks before priority rows load; errorMessage blocks content |

## The Error-Message-Blocking-Content Trap

**Pattern:** When `errorMessage != null` is checked BEFORE `else` in a `when` block, it blocks ALL content including priority rows that don't need the failing API.

```kotlin
// BROKEN — blocks everything, including Continue Watching, hero, etc.
when {
    uiState.isLoading -> LoadingSpinner()
    uiState.errorMessage != null -> ErrorMessage(message)  // blocks all content
    else -> LazyColumn { /* catalog rows + priority rows */ }
}
```

**Fix:** Never set `errorMessage` for non-critical failures (missing API key). Instead:
1. Use `catalogsUnavailable: Boolean` flag
2. Show a non-blocking banner INSIDE the LazyColumn
3. Keep priority rows (Continue Watching, Watchlist, etc.) always visible

```kotlin
// FIXED — banner in content, not blocking everything
if (uiState.catalogsUnavailable) {
    item {
        Row(Modifier.background(accent.copy(alpha = 0.12f), RoundedCornerShape(8.dp))) {
            Text("⚠ Catalogs Unavailable")
            Text("Add TMDB_API_KEY to local.properties...")
        }
    }
}
// Priority rows + hero render normally below
```

## Stremio Addon Fallback (NuvioTV Pattern)

When TMDB is down or key is invalid, load catalogs from installed Stremio addon manifests:

```kotlin
// In loadHome(), after catalog rows resolve empty:
if (!hasCatalogItems) {
    val addonRows = loadAddonCatalogRows(profile, watchedIds)
    // Append to catalogRows
}

private suspend fun loadAddonCatalogRows(...): List<RowLoadResult> {
    val catalogAddons = addonRepository.getCatalogAddons()
        .filter { it.manifest != null }
    // For each addon, extract catalogs from manifest
    // Query each addon's /catalog/{type}/{catalogId}.json
    // Return RowLoadResult for each
}
```

Key: Stremio addons use their OWN catalog IDs (e.g., `top`, `movies.popular`), not TMDB's (`tmdb.trending_day`). Query them with the IDs from the addon's manifest.

## fetchCatalogSafe Tiered Fallback

The `fetchCatalogSafe()` function should use the tiered chain, not call TMDB directly:

```kotlin
private suspend fun fetchCatalogSafe(type, catalogId, extra): List<MetaPreview> {
    // Primary: AddonRepository tiered chain (TMDB → Worker → Stremio addons)
    val addonResult = addonRepository.getCatalogRows(type, catalogId, extra)
        .getOrElse { emptyList() }
        .map { normalize(it) }
    if (addonResult.isNotEmpty()) return deduplicate(addonResult)

    // Last resort: direct TMDB call
    if (catalogId.startsWith("tmdb.")) {
        return deduplicate(tmdbCatalogRepository.getCatalogRow(type, catalogId, extra)
            .map { normalize(it) })
    }
    return emptyList()
}
```
