# APK-Native Embed Source Provider

How embed sources (free, no debrid needed) are integrated into the Unspooled APK via `EmbeddedEmbedSourceProvider`.

## Architecture

```
ProviderRegistry
├── alwaysAvailable: Map<String, StreamProvider>        ← embed sources
│   └── embed-sources → EmbeddedEmbedSourceProvider
└── debridRequired: Map<String, Lazy<StreamProvider>>   ← debrid scrapers
    └── stream-scraper → ScraperProvider (torrents)
```

Both tracks merge in `enabledProviders()`. Embed sources are always returned; debrid scrapers only when a debrid account is connected.

## EmbeddedEmbedSourceProvider

**File**: `~/Unspooled/app/src/main/java/com/unspooled/app/data/provider/embed/EmbeddedEmbedSourceProvider.kt`

Implements `StreamProvider` with:
- `metadata.priority = 10` — lower than debrid scrapers (priority 20)
- `isAvailable() = true` — always available
- `getStreams()` — generates Stream objects from TMDB/IMDB IDs
- **Zero external dependencies** — `@Inject constructor()` parses IDs inline

### ID Parsing (No TMDB API Needed)

The provider parses raw `type`/`id` strings directly — no `ScrapeContextResolver` or TMDB API calls:

```
tt0133093          → imdbId="tt0133093", tmdbId=null
tt0944947:1:1      → imdbId="tt0944947", season=1, episode=1
tmdb:603           → imdbId=null, tmdbId=603
603 (numeric)      → imdbId=null, tmdbId=603
tmdb:tv:1399:1:1   → imdbId=null, tmdbId=1399, season=1, episode=1
```

**DO NOT** use `ScrapeContextResolver` or any TMDB-dependent class. The embed URLs only need the raw ID — title/year metadata is cosmetic. Depending on TMDB causes the provider to silently return zero sources when TMDB is down or misconfigured.

### Embed Sources Generated

| Source | URL Pattern | Quality |
|--------|-------------|---------|
| MultiEmbed | `https://multiembed.mov/?video_id={tmdbId}&tmdb=1` | 1080p |
| VidSrc | `https://vidsrc.to/embed/{type}/{imdbId}` | 1080p |
| 2Embed | `https://www.2embed.cc/embed/{imdbId}` | 720p |
| EmbedSu | `https://embed.su/embed/{type}/{imdbId}` | 720p |
| VidBinge | `https://vidbinge.dev/embed/{type}/{imdbId}` | 720p |

## Critical: `url = null` for Embed Sources

**Embed page URLs are NOT video streams.** ExoPlayer cannot parse them. Attempting leads to:

```
UnrecognizedInputFormatException: None of the available extractors (...)
could read the stream.
```

Fix: `url` is intentionally `null` on embed Stream objects. The original embed URL is stored in `description` prefixed with `embed://`:

```kotlin
private fun embedStream(url: String, name: String, ...): Stream = Stream(
    url = null,  // MUST be null — prevents ExoPlayer crash
    name = name,
    description = "embed://${url}",  // Recoverable via prefix
    addonName = addonName,
    addonId = "embed-sources",
    isDebridCached = false,
    behaviorHints = StreamBehaviorHints(notWebReady = true),
)
```

The embed URL is recoverable via `EmbeddedEmbedSourceProvider.extractEmbedUrl()`:
```kotlin
fun extractEmbedUrl(stream: Stream): String? {
    val desc = stream.description ?: return null
    if (!desc.startsWith("embed://")) return null
    return desc.removePrefix("embed://")
}
```

## Playback via Server Proxy

When the user selects an embed source, the player should:
1. Extract the embed URL from `stream.description` (strip `embed://` prefix)
2. Call `BuildConfig.NEXSTREAM_SCRAPER_URL/api/embed/proxy?url=EMBED_URL`
3. The server resolves the embed page to a direct video URL (m3u8/mp4) and streams it
4. ExoPlayer plays the proxied stream

The server's `/api/embed/proxy` endpoint auto-detects whether the URL is:
- A direct video URL (has `.m3u8`, `.mp4`, `.ts` extension) → proxies directly
- An embed page URL (multiembed.mov, vidsrc.to, etc.) → resolves first via the extractor registry, then proxies the resulting stream

## ProviderRegistry Dual-Track

```kotlin
private val alwaysAvailable: Map<String, StreamProvider> = mapOf(
    "embed-sources" to embedProvider,
)

private val debridRequired: Map<String, dagger.Lazy<out StreamProvider>> = mapOf(
    "stream-scraper" to scraperProvider,
)

fun enabledProviders(): List<StreamProvider> {
    val providers = mutableListOf<StreamProvider>()
    for ((_, provider) in alwaysAvailable) providers.add(provider)
    if (isDebridConnected()) {
        for ((id, lazyProvider) in debridRequired) {
            if (isProviderEnabled(id)) providers.add(lazyProvider.get())
        }
    }
    return providers.sortedByDescending { it.metadata.priority }
}
```

## Auto-Play Behavior

`ResolvePlaybackSourceUseCase` must skip embed sources when auto-selecting the best stream:

```kotlin
val bestPlayable = ranked.firstOrNull { rankedStream ->
    val s = rankedStream.stream
    s.infoHash != null || !s.debridUrl.isNullOrBlank() || !s.url.isNullOrBlank()
} ?: ranked.firstOrNull { it.stream.url != null }
    ?: ranked.first()
```

Embed sources have `url = null` so they fail all three null-checks. Debrid streams (which have `infoHash` or `url`) are preferred.

## UI Integration

In `StreamSection`:
- `debridStreams` = streams with `isDebridCached == true`
- `freeStreams` = streams with `isDebridCached == false` ← embed sources here
- Embed sources show as "Free" with their provider name (e.g. `MultiEmbed`, `VidSrc`)

In `AllSourcesScreen.SourceListItem`:
- Shows `stream.name` as the source title (e.g. `[EMBED] MultiEmbed 1080p`)
- Shows `stream.addonName` as the provider label
- Badges render from quality/cache properties

## `notWebReady` Gap

`StreamBehaviorHints.notWebReady` is defined in the model (`Models.kt:218`) and set to `true` on embed sources, but **no code in the app reads this field**. It's a Stremio protocol field meant to signal non-direct-playability. Future UI work should check this flag.

## Test Changes

The existing `ProviderRegistryProvidedSourcesTest` was rewritten when embed sources were added:
- Old test asserted `providers.isEmpty()` without debrid → FAILED after embed sources added
- New test asserts: embed sources present without debrid, more providers appear with debrid
- Requires mocking `EmbeddedEmbedSourceProvider.metadata` and `coEvery { isAvailable() }`

## Pitfalls

- **Embed URLs are NOT direct video URLs.** ExoPlayer cannot play them. `url` MUST be `null`. Store in `description`.
- **Server proxy required for playback.** Without `NEXSTREAM_SCRAPER_URL`, embed sources are informational only.
- **URL patterns change.** Embed sites frequently change domains. Test periodically.
- **Port mismatch kills the server connection.** Docker nexstream-scraper used `HTTPS_PORT=3443` with `https://`. Standalone ultimate-scraper uses `PORT=3091` with `http://`. `NEXSTREAM_SCRAPER_URL` in `local.properties` MUST match the active server.
- **`behaviorHints.notWebReady` is defined but UNCHECKED.** The app should eventually read it for proper UX.
- **`ProviderRegistryProvidedSourcesTest` must be updated** when adding/removing always-available providers.
- **Auto-play silently fails without URL-filtering.** If `ResolvePlaybackSourceUseCase` picks an embed source (null url), both debrid and fallback paths return null → user sees "no sources available" with no error message.
