# Real-Debrid Torrent Lifecycle — Android/Kotlin Client

## The Bug

`RealDebridClient.getDownloadUrl()` was broken. It called `addMagnet`, then **immediately** called `getTorrentInfo` with zero polling. RD torrents don't resolve instantly — they go through a state machine — so `links` was always null, causing `IllegalStateException("No links available")`. **100% failure rate for uncached magnets.**

## RD Torrent State Machine

```
addMagnet → magnet_conversion → waiting_files_selection → downloading → downloaded
                                  ↑ selectFiles("all")         ↑ unrestrictLink
```

### States

| State | Meaning | Action |
|-------|---------|--------|
| `magnet_conversion` | Resolving metadata | Poll, wait |
| `queued` | In queue | Poll, wait |
| `waiting_files_selection` | Needs file selection | Call `selectFiles("all")` |
| `downloading` | Transferring to RD servers | Poll, wait |
| `downloaded` | Ready, `links` populated | Unrestrict the link |
| `error`, `magnet_error`, `dead`, `virus` | Terminal failure | Throw |

## The Fix (Kotlin)

### 1. Add `selectFiles` to Retrofit interface

```kotlin
// RealDebridApi.kt
import retrofit2.Response

@POST("torrents/selectFiles/{id}")
@FormUrlEncoded
suspend fun selectFiles(
    @Header("Authorization") auth: String,
    @Path("id") id: String,
    @Field("files") files: String = "all",
): Response<Unit>
```

### 2. Rewrite `getDownloadUrl()` with polling

```kotlin
companion object {
    private const val INITIAL_POLL_INTERVAL_MS = 2_000L
    private const val MAX_POLL_INTERVAL_MS = 8_000L
    private const val POLL_BACKOFF_FACTOR = 1.5
    private const val MAX_POLL_TOTAL_MS = 120_000L  // 2 min
    private val TERMINAL_ERROR_STATUSES = setOf("error", "magnet_error", "dead", "virus")
}

override suspend fun getDownloadUrl(magnetOrHash: String): String {
    val magnet = if (magnetOrHash.length == 40) {
        "magnet:?xt=urn:btih:$magnetOrHash"
    } else {
        magnetOrHash
    }
    val auth = authHeader()

    // 1. Add magnet
    val addResult = retrofitApi.addMagnet(auth, magnet)
    val torrentId = addResult.id
    check(torrentId.isNotBlank()) { "Failed to add magnet: no torrent ID returned" }

    // 2. Poll until downloaded
    val startTime = System.currentTimeMillis()
    var intervalMs = INITIAL_POLL_INTERVAL_MS
    var filesSelected = false

    while (true) {
        val info = retrofitApi.getTorrentInfo(auth, torrentId)
        val status = info.status

        when {
            status in TERMINAL_ERROR_STATUSES ->
                throw IllegalStateException("Torrent $torrentId terminal: $status")

            status == "downloaded" -> {
                val link = info.links?.firstOrNull()
                    ?: throw IllegalStateException("No links")
                val result = retrofitApi.unrestrictLink(auth, link)
                return result.download ?: result.link
                    ?: throw IllegalStateException("No download URL")
            }

            status == "waiting_files_selection" && !filesSelected -> {
                retrofitApi.selectFiles(auth, torrentId, "all")
                filesSelected = true
            }
        }

        if (System.currentTimeMillis() - startTime >= MAX_POLL_TOTAL_MS) {
            throw IllegalStateException("Timed out after ${MAX_POLL_TOTAL_MS}ms (status=$status)")
        }

        delay(intervalMs)
        intervalMs = (intervalMs * POLL_BACKOFF_FACTOR).toLong()
            .coerceAtMost(MAX_POLL_INTERVAL_MS)
    }
}
```

### 3. Don't forget the import

```kotlin
import kotlinx.coroutines.delay
```

## Why the Polling Backoff

- **2s initial**: Fast enough for well-seeded torrents
- **8s max**: Don't hammer RD's API
- **1.5x factor**: Smooth ramp — 2, 3, 4.5, 6.75, 8, 8, 8...
- **120s total**: Most torrents resolve in 10-30s. Timeout catches stuck ones without blocking forever.

## Other Providers

AllDebrid, Premiumize, and TorBox have different state machines and APIs. Don't assume the RD pattern works for them. Check each provider's docs for their specific torrent lifecycle and file selection API.
