# Cloud Manager Direct Playback Pattern (2026-05-20)

Use this when finishing Debrid Cloud / account-library features in NexStream.

## Goal
Let users browse provider cloud/transfers and play ready files without copying competitor APK code, without adding scraper fleets, and without persisting/logging tokenized provider URLs.

## Architecture pattern

1. Keep the UI provider-neutral:
   - `DebridCloudItem(provider, id, name, status, progress, sizeBytes, fileCount, playableUrl)`
   - UI shows account cards, cloud item rows, and a detail panel.

2. Add provider hooks through the existing Debrid abstraction:
   - `DebridApi.listCloudItems(): List<DebridCloudItem> = emptyList()`
   - `DebridApi.resolveCloudItem(item): String? = item.playableUrl`
   - `DebridService.listCloudItems(provider)` wraps provider exceptions and returns empty on failure.
   - `DebridService.resolveCloudItem(item)` resolves in memory only; never persist unrestricted URLs.

3. Provider mapping used this session:
   - Real-Debrid: `GET torrents` lists account torrents. Cloud `playableUrl` may be a provider link and must be passed through `unrestrict/link` in `RealDebridClient.resolveCloudItem()` before playback.
   - Premiumize: `transfer/list` provides transfer status; direct playable file URLs require a later item/details/directdl drill-down if not included.
   - TorBox: `mylist` exposes torrents/files; use file `download_url` only when present.
   - AllDebrid: leave default empty until a documented safe cloud endpoint is added.

## Navigation pattern

Add a dedicated direct-player route instead of pretending cloud files are catalog sources:

```kotlin
data object DirectPlayer : Screen("player-direct?url={url}&title={title}") {
    fun createRoute(url: String, title: String = "Cloud playback") =
        "player-direct?url=${Uri.encode(url)}&title=${Uri.encode(title)}"
}
```

NavHost route:

```kotlin
composable(
    route = Screen.DirectPlayer.route,
    arguments = listOf(
        navArgument("url") { type = NavType.StringType; defaultValue = "" },
        navArgument("title") { type = NavType.StringType; defaultValue = "Cloud playback" },
    ),
) { entry ->
    PlayerScreen(
        mediaType = "cloud",
        mediaId = entry.arguments?.getString("url") ?: "",
        streamIndex = 0,
        navController = navController,
        directTitle = entry.arguments?.getString("title") ?: "Cloud playback",
    )
}
```

`PlayerViewModel.initialize()` should treat `mediaType == "cloud"` as direct URL playback, set a cloud-friendly title/source label, and skip addon stream lookup.

## UI state pattern

`CloudManagerUiState` should include:

```kotlin
val selectedItem: DebridCloudItem? = null
val playbackTarget: CloudPlaybackTarget? = null
val isResolvingPlayback: Boolean = false
```

ViewModel flow:

1. User selects item -> set `selectedItem` and show detail panel.
2. User presses Play -> set `isResolvingPlayback=true`, call `debridService.resolveCloudItem(item)`.
3. If null -> show "not playable yet" error; likely unfinished transfer.
4. If URL -> set one-shot `playbackTarget`.
5. UI `LaunchedEffect(playbackTarget)` navigates to `DirectPlayer` and then calls `consumePlaybackTarget()`.

## Security / privacy rules

- Do not log full cloud URLs, unrestricted URLs, tokens, or provider paths.
- Do not store unrestricted URLs in Room/DataStore.
- Do not add destructive delete actions until provider-specific UX and endpoint behavior are confirmed.
- Keep cloud links in memory as one-shot playback targets.

## Build verification

After each batch:

```bash
./gradlew testDebugUnitTest compileDebugKotlin --console=plain
```

Final:

```bash
./gradlew assembleDebug testDebugUnitTest --console=plain
```

This session ended green with `BUILD SUCCESSFUL` after direct cloud browse/resolve/play work.