# Unspooled Debrid Connection Flow — End-to-End Audit

## User-Facing Connection (3 paths, all work correctly)

### Path 1: API Key Entry
1. User goes to Settings → Debrid Services
2. `DebridAuthViewModel.promptApiKeyEntry()` → UI shows API key text field
3. `DebridAuthViewModel.submitApiKey(provider, apiKey)`
4. `DebridService.connect(provider, token, verify=true)`
5. Token stored in `SecureTokenStore` (Android Keystore + EncryptedFile, AES-256-GCM at rest)
6. `connectedProviders` StateFlow updated
7. `addonRepository.refreshDebridEngines()` triggers AIOStreams reconfiguration

### Path 2: OAuth Device Flow
1. User taps "Device Login" for RD/AD/PM
2. `DebridAuthViewModel.connectWithDeviceAuth(provider)`
3. `DebridAuthManager.startDeviceAuth(provider)` — calls provider's device-code endpoint
4. UI shows `userCode` + `verificationUrl` — user enters code on provider's website
5. `DebridAuthManager.pollForToken()` — polls every 5-15s with deadline
6. Handles: `slow_down` (backoff), `expired` (stop), `authorization_pending` (continue), network errors (retry)
7. On success: `handleTokenReady(provider, token)` → `debridService.connect(provider, token)`
8. Real-Debrid OAuth v2 stores both `access_token` AND `client_secret` (for renewal)

### Path 3: QR Cloud Auth
1. `DebridAuthViewModel.startQrAuthFlow(provider)`
2. `DebridAuthApiClient.createSession()` → `POST {worker}/api/debrid/auth-sessions`
3. QR code shows `{worker}/debrid/auth/{code}` — user scans with phone
4. Phone browser: enters API key → `POST {worker}/debrid/auth/submit/{code}`
5. TV polls → gets `configuredApiKey` → `debridService.connect()`

## Token Storage Chain (SecureTokenStore)

```
User connects → DebridService.connect()
  → DebridTokenStorage.saveToken(provider, token)
    → SecureTokenStore.storeBlocking("debrid_token_real_debrid", token)
      → Android Keystore (key generation, hardware-backed)
        → EncryptedFile (AES-256-GCM, file on disk)

DebridTokenStorage aliases:
  token:   "debrid_token_" + provider.name.lowercase()
  secret:  "debrid_secret_" + provider.name.lowercase()   (RD OAuth client_secret)
  refresh: "debrid_refresh_" + provider.name.lowercase()  (RD OAuth refresh_token)
```

## How the Scraper Uses User Tokens

The scraper is **source-only** — it does NOT know the user's debrid token. The token stays on-device:

```
User plays content → AddonRepository.getStreamsForContent()
  → NexStream scraper: returns magnet links + metadata (NO debrid resolution)
  → AIOStreams: returns magnets, manifest URL carries ?realdebrid=TOKEN

→ DebridEnricher.enrich(streams)
  → streams with infoHash only → partition
  → DebridService.checkCachePerProvider(uniqueHashes)
    → for each connected provider:
      → RealDebridClient.checkInstantAvailability(hashes)
        → authHeader() reads token from SecureTokenStore.getTokenBlocking(REAL_DEBRID)
        → calls GET /rest/1.0/torrents/instantAvailability/{hash}
      → AllDebridClient: paginates hash batches by /magnet/instant endpoint
      → PremiumizeClient: /cache/check with hashes[] body
      → TorBoxClient: /api/torrents/checkcache with hashes
    → returns Map<DebridProvider, Map<hash, InstantAvailability>>
  → annotates streams: isDebridCached=true/false, debridProvider=name, healthScore

→ PlaybackOrchestrator.prepare(streams)
  → for each stream candidate:
    → if stream has url/debridUrl: validates via HEAD request
    → if stream has infoHash only:
      → DebridService.resolveStream(stream, provider)
        → provider client: RealDebridClient.getDownloadUrl(hash)
          → POST /rest/1.0/torrents/addMagnet (if not already added)
          → GET /rest/1.0/torrents/info/{id} (poll until status=downloaded)
          → GET /rest/1.0/unrestrict/link
          → returns direct HTTPS URL
        → PremiumizeClient: /transfer/create → /transfer/list → /transfer/directdl → URL
        → AllDebridClient: /magnet/upload → /magnet/status → /link/unlock → URL
        → TorBoxClient: /api/torrents/create → poll → /api/torrents/mylist → URL
  → ExoPlayer plays the direct HTTPS URL

→ PlayerViewModel.onPlayerError()
  → FallbackEngine.getFallbackStream()
  → plays next candidate from rankedStreams
```

## Bug Found: DebridSettingsViewModel Orphaned Storage

`DebridSettingsViewModel` stores API keys in `DebridSettingsDataStore` (plain Android DataStore XML). 
`DebridService` reads from `DebridTokenStorage` (SecureTokenStore).
These are **two separate storage systems** — API keys saved through settings never reached the streaming pipeline.

### Fix Pattern
```kotlin
// Inject DebridService into DebridSettingsViewModel
class DebridSettingsViewModel @Inject constructor(
    private val dataStore: DebridSettingsDataStore,
    private val debridService: DebridService,  // ← NEW
) : ViewModel() {

    // Every API key setter must also propagate to DebridService
    fun setRealDebridApiKey(apiKey: String) {
        update { dataStore.setRealDebridApiKey(apiKey); syncToDebridService(DebridProvider.REAL_DEBRID, apiKey) }
    }

    private fun syncToDebridService(provider: DebridProvider, key: String) {
        viewModelScope.launch {
            val trimmed = key.trim()
            if (trimmed.isBlank()) {
                debridService.disconnect(provider)
            } else {
                debridService.connect(provider, trimmed, verify = false)
            }
        }
    }
}
```

## DebridAuthManager — OAuth v2 Implementation Notes

### Real-Debrid Flow
1. `requestDeviceCode(clientId)` → returns `device_code`, `user_code`, `verification_url`, `interval`, `expires_in`
2. User visits `https://real-debrid.com/device` → enters `user_code`
3. Poll `requestDeviceCredentials(clientId, deviceCode)` every 5s
   - 200: returns `client_id` + `client_secret` → proceed to token exchange
   - 400 "authorization_pending": continue polling
   - 400 "slow_down": add 5s to interval
   - 400 "expired": stop, return null
   - 403: log, continue polling (credentials not yet ready)
   - 429: add 5s to interval, continue
   - Network error: continue polling
4. `exchangeDeviceCode(clientId, clientSecret, deviceCode)` → returns `access_token` + `refresh_token`
5. Store `access_token` as API token, store `client_secret` + `refresh_token` for renewal

### AllDebrid Flow
1. `requestPin()` → returns `pin`, `user_url`, `check_every`, `expires_in`
2. User visits `https://alldebrid.com/pin` → enters pin
3. Poll `pollPin(pin)` every N seconds until `data.activated == true`
4. Returns `data.apikey`

### Premiumize Flow
1. `requestDeviceCode(clientId)` → returns `device_code`, `user_code`, `verification_uri`, `interval`, `expires_in`
2. User visits verification URI → enters `user_code`
3. Poll `pollDeviceToken(deviceCode, clientId)` every N seconds
4. Handles: `authorization_pending` (continue), `slow_down` (backoff)
5. Returns `access_token`

## DebridSettingsDataStore (Orphaned — Fixed)

`DebridSettingsDataStore` persists `DebridSettings` (preferences like stream quality, sort mode, templates) as JSON in Android DataStore. It also had API key fields that were never propagated to `DebridService`. 

**Current state:** `DebridSettingsViewModel` now wires API key changes to `DebridService.connect()`. The duplicate API key storage in `DataStore` remains for backward compatibility but is secondary to `SecureTokenStore`.

Settings that ARE only in DataStore (not in SecureTokenStore):
- `enabled`, `cloudLibraryEnabled` — feature toggles
- `preferredResolverProviderId` — which debrid to prefer for resolves
- `streamMaxResults`, `streamSortMode`, `streamMinimumQuality` — quality preferences
- `streamDolbyVisionFilter`, `streamHdrFilter`, `streamCodecFilter` — codec filters
- `streamNameTemplate`, `streamDescriptionTemplate` — display templates
