# Modular Stream Provider Architecture for Android TV

Reusable pattern for a multi-provider stream aggregation system in an Android TV app, inspired by AIOStreams, Comet, and MediaFusion.

## Interface Hierarchy

```kotlin
// ── StreamProvider — any source that returns streams ──
interface StreamProvider {
    val metadata: ProviderMetadata
    suspend fun getStreams(type: String, id: String): Result<List<Stream>>
    suspend fun isAvailable(): Boolean
}

// ── ProviderMetadata — descriptor for discovery ──
data class ProviderMetadata(
    val id: String,
    val name: String,
    val baseUrl: String,
    val supportedServices: List<DebridProvider>,
    val supportedResources: List<String>,
    val priority: Int = 0,
)
```

## ProviderRegistry (Fan-Out Aggregator)

```kotlin
@Singleton
class ProviderRegistry @Inject constructor(
    aiostreamsProvider: dagger.Lazy<AIOStreamsProvider>,
    torrentioProvider: dagger.Lazy<TorrentioProvider>,
    cometProvider: dagger.Lazy<CometProvider>,
    mediaFusionProvider: dagger.Lazy<MediaFusionProvider>,
    registryRepo: AddonRegistryRepository,
) {
    fun enabledProviders(): List<StreamProvider>  // filtered by user preferences
    suspend fun fetchAllStreams(type, id): Result<List<Stream>>  // fan-out
}
```

Key design decisions:
- `dagger.Lazy<T>` for each provider — avoids circular DI issues with Hilt/KSP
- Providers are instantiated eagerly but queries only happen at call time
- User enable/disable preferences stored separately, never overwritten by remote registry
- Sequential queries (not parallel) — acceptable for 2-4 providers; add `coroutineScope { async {} }` for 5+

## DebridEnricher (Missing Pipeline Stage)

The gap: Torrentio without a debrid token returns P2P streams with `infoHash` but **no URL**. The orchestrator needs a URL to play. The DebridEnricher bridges this:

```kotlin
@Singleton
class DebridEnricher @Inject constructor(
    private val debridService: DebridService,
) {
    suspend fun enrich(streams: List<Stream>): List<Stream> {
        // 1. Separate: P2P (infoHash, no url) vs resolved (has url)
        val (p2p, resolved) = streams.partition { it.infoHash != null && it.url.isNullOrBlank() }
        if (p2p.isEmpty()) return streams

        // 2. Fan-out: check all active debrid services for cache status
        val results = debridService.checkCachePerProvider(uniqueHashes)

        // 3. Enrich: set isDebridCached, debridProvider, healthScore
        return enriched + resolved
    }
}
```

## DebridService Extension for Batch Cache Check

Add a public method to `DebridService` for per-provider, per-hash cache checking:

```kotlin
suspend fun checkCachePerProvider(
    hashes: List<String>,
): Map<DebridProvider, Map<String, InstantAvailability>>
```

Also expose `getClient(provider)` as `public` (was `private`) so enrichers can access individual provider clients.

## PlaybackOrchestrator Lazy Debrid Resolution

Extend `PlaybackOrchestrator.prepare()` to handle streams where both `debridUrl` and `url` are null but `infoHash` and `isDebridCached` are set:

```kotlin
val url = when {
    !stream.debridUrl.isNullOrBlank() -> stream.debridUrl
    !stream.url.isNullOrBlank() -> stream.url
    stream.infoHash != null && stream.isDebridCached -> {
        debridService.resolveStream(stream, provider)  // lazy resolve
    }
    else -> { failures += "no URL"; continue }
}
```

## Full Pipeline

```
User selects content
  → ProviderRegistry.fetchAllStreams()  # only enabled addons
  → [AIOStreamsProvider, CometProvider, ...]
  → DebridEnricher.enrich()            # bridges infoHash → debrid cache
  → StreamFilterEngine.filter()        # user regex/property filters
  → SourceRanker.rankStreams()         # cached-first, quality-based
  → UI: SourceListState
  → Play: PlaybackOrchestrator.prepare() → lazy debrid resolve
```

## Pitfalls

- **Double enrichment**: Ensure enrichment happens only once. Don't enrich in individual providers AND in the aggregator. Single enrichment point in the repository layer.
- **Hilt/KSP circular deps**: Use `dagger.Lazy<T>` for new `@Inject` classes referenced in constructors of other `@Inject` classes. KSP may not resolve the symbol on first pass.
- **Dead addons**: Always audit addon URLs with curl before shipping. Domains drift, ElfHosted instances get retired. Remove dead entries from SYSTEM_ADDONS and marketplace immediately.
- **NoTorrent trap**: The `.stremio-addon.app` TLD appears abandoned. Several addons on this TLD returned NXDOMAIN. Don't ship without verification.
