# Poster Image Pipeline — 2026-05-30

## End-to-End Flow

```
DATA LAYER                         NORMALIZER                    UI LAYER
─────────────                      ────────────                  ─────────
TMDB API → TmdbResultItem          CatalogImageNormalizer        CatalogPosterImage
  posterPath: "/abc.jpg"             normalize(raw, kind)          AsyncImage(model=request)
  toMetaPreview() → w342 URL        ┌─ null/blank → null          Coil fetches URL
                                    │─ https:// → passthrough     Disk cache: image_cache_v2/
Trakt API → TraktTrendingMovieItem  │  + stripTrailingQuery()     Memory cache: 25% heap
  images.poster → Trakt CDN URL     │  + resizeTmdbIfNeeded()     Error → gradient placeholder
  toMetaPreview() → raw URL         │─ /path → base + path        Logs failure with URL
                                    │─ no-slash → "/" + path
Kitsu API → KitsuAnimeResource      └─ //path → https: + path
  posterImage.large.url
  → raw URL
```

## Critical Rule: Sizes Matter

| Context | Poster Size | Backdrop Size |
|---------|------------|--------------|
| Catalog rows | **w342** | w780 |
| Detail poster | w500 | — |
| Hero backdrop | — | **w1280** |
| Row backdrop | — | w780 |

**Do NOT use `w500` or `w1280` at the catalog `toMetaPreview()` data boundary.** The normalizer resizes later, but the intermediate URL change causes Coil disk cache key mismatches — cache stores w500, loads w342, incurs re-download.

## Coil Cache Stability

Three factors that break Coil cache:

1. **URL changes between fetches** — if `toMetaPreview()` produces w500 and normalizer resizes to w342, different URLs hit disk
2. **Query parameters** (`?w=300`) — stripped by `stripTrailingQuery()` in normalizer
3. **Different cache keys** — `MediaCard` passes `cacheKey = "poster_${item.id}"` which is stable, but the disk cache is keyed by the URL

### Versioning the cache directory

When fixing a poster regression, bump the cache dir name to clear old broken entries:
```kotlin
val versionedDir = context.cacheDir.resolve("image_cache_v2")
DiskCache.Builder().directory(versionedDir.absolutePath.toPath())
```

## Regression Debugging Checklist

When posters stop loading or load slowly:

1. **Check TMDB `toMetaPreview()` sizes** — are they w342/w780 or w500/w1280?
2. **Check normalizer is called** — is `normalizeMetaPreview()` applied before display?
3. **Check Coil cache keys** — are they stable across recomposition?
4. **Check for query params** — `?w=300&h=450` on URLs
5. **Check for cache version bump** — old `image_cache` may have broken entries
6. **Check null-overwrite guards** — does new null poster overwrite existing valid?

## Trakt Poster Routing

**Do NOT try to resolve TMDB poster URLs from Trakt IDs at the catalog repository level.** It would require an extra API call per item (slow). Trakt CDN URLs (`walter.trakt.tv`) are stable and pass through the normalizer unchanged. Let Coil handle the fetch.

## Build Commands

```bash
# Run poster normalizer tests
./gradlew :app:testDebugUnitTest --tests "com.nexstream.app.data.image.CatalogImageNormalizerTest"

# Build debug APK
./gradlew :app:assembleDebug
```

## Key Files

- `data/image/CatalogImageNormalizer.kt` — central normalizer (TMDB paths, URL passthrough, http→https, query stripping, size resizing)
- `data/tmdb/TmdbApi.kt` — `TmdbResultItem.toMetaPreview()` and `TmdbSimilarResult.toMetaPreview()` set poster/backdrop sizes
- `ui/components/CatalogPosterImage.kt` — Coil AsyncImage wrapper with gradient placeholder + error logging
- `data/CoilModule.kt` — Coil ImageLoader config (OkHttp client, disk/memory caches)
- `data/catalog/TraktCatalogRepository.kt` — uses raw Trakt images (no TMDB resolution at repo level)
