diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt index 04100dd..bc392c5 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt @@ -52,6 +52,7 @@ import pw.binom.viewmate.core.media.MirrorJob import pw.binom.viewmate.core.protocol.DownloadPhase import pw.binom.viewmate.phone.PhoneApp import pw.binom.viewmate.phone.WatchProgress +import pw.binom.viewmate.phone.log /** Вкладки каталога: Фильмы / Сериалы / Папки. */ enum class CatalogTab { MOVIES, SERIES, FOLDERS } @@ -205,7 +206,6 @@ class CatalogState( } } items = loaded - mirrors = app.mirror.list(limit = 500).associateBy { it.itemId.orEmpty() } } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -214,6 +214,15 @@ class CatalogState( } finally { loading = false } + // Зеркала — отдельный «мягкий» запрос: сбой/таймаут mirror.list не чистит + // уже загруженный каталог, просто временно без бейджей. + try { + mirrors = app.mirror.list(limit = 500).associateBy { it.itemId.orEmpty() } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + log("catalog", "зеркала: не удалось (${e.message}) — каталог без бейджей") + } } } diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt index 129600c..8010b1c 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt @@ -78,16 +78,16 @@ fun DetailsScreen( val glassesDownloadedIds by app.server.hub.downloadedItemIds.collectAsState() val glassesMediaDir by app.server.hub.mediaDir.collectAsState() - // статус зеркала + дорожки; пока конвертируется — опрашиваем + // статус зеркала + дорожки; пока конвертируется — опрашиваем только дешёвый byItem. + // Тяжёлый list(limit=500) НЕ дёргаем каждые 3 с: progress зеркала на сервере не живой + // (0 до конца), а тяжёлый запрос с большим лимитом — главный источник таймаутов. LaunchedEffect(item.Id, refreshKey) { loading = true mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull() - mirrorJob = runCatching { app.mirror.list(limit = 500).firstOrNull { it.itemId == item.Id } }.getOrNull() loading = false while (mirrorStatus?.status == "processing" || mirrorStatus?.status == "queued") { delay(3_000) mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull() - mirrorJob = runCatching { app.mirror.list(limit = 500).firstOrNull { it.itemId == item.Id } }.getOrNull() } } diff --git a/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/HttpClient.kt b/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/HttpClient.kt index c9fddcd..3d39376 100644 --- a/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/HttpClient.kt +++ b/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/HttpClient.kt @@ -42,6 +42,35 @@ fun defaultHttpClient(): HttpClient = HttpClient { } } +/** + * HTTP-клиент для media-mirror: тяжёлые опросы (mirror.list с большим лимитом) + * на сервере считаются заметно дольше, чем обычные запросы, поэтому таймауты + * шире [defaultHttpClient] — иначе живой, но медленный ответ упирается в таймаут. + */ +fun defaultMirrorHttpClient(): HttpClient = HttpClient { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + explicitNulls = false + } + ) + } + install(HttpTimeout) { + connectTimeoutMillis = 15_000 + requestTimeoutMillis = 90_000 + } + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + if (HttpClientDebugLogging.enabled) println("[view-mate] $message") + } + } + level = LogLevel.INFO + } +} + /** Флаг debug-логирования HTTP (по умолчанию включён). */ object HttpClientDebugLogging { var enabled: Boolean = true diff --git a/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/MirrorClient.kt b/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/MirrorClient.kt index cbc9622..4688739 100644 --- a/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/MirrorClient.kt +++ b/lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/MirrorClient.kt @@ -2,6 +2,8 @@ package pw.binom.viewmate.core.media import io.ktor.client.HttpClient import io.ktor.client.call.body +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.header @@ -19,7 +21,7 @@ import io.ktor.http.contentType class MirrorClient( private val baseUrl: String, // https://mirror.binom.pw private val apiKey: String, // X-API-Key - private val http: HttpClient = defaultHttpClient(), + private val http: HttpClient = defaultMirrorHttpClient(), ) { /** Заказ конвертации элемента. 201 — создан, 200 — уже есть (дубль). */ suspend fun createMirror(itemId: String): MirrorJob { @@ -42,15 +44,26 @@ class MirrorClient( return response.body() } - /** Список зеркал: статус (null — все) + лимит. */ + /** Тяжёлый запрос (большой limit, медленный сервер): повторяем при сбое. Статус (null — все) + лимит. */ suspend fun list(status: String? = null, limit: Int = 50): List { - val response = http.get("$baseUrl/api/mirror") { - header("X-API-Key", apiKey) - parameter("limit", limit) - if (status != null) parameter("status", status) + var last: Exception? = null + repeat(LIST_ATTEMPTS) { attempt -> + try { + val response = http.get("$baseUrl/api/mirror") { + header("X-API-Key", apiKey) + parameter("limit", limit) + if (status != null) parameter("status", status) + } + response.ensureSuccess("mirror.list") + return response.body() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + last = e + if (attempt < LIST_ATTEMPTS - 1) delay(LIST_RETRY_DELAY_MS) + } } - response.ensureSuccess("mirror.list") - return response.body() + throw ApiClientException("mirror.list не удался: ${last?.message}", cause = last) } /** Удаление зеркала (204). */ @@ -69,4 +82,11 @@ class MirrorClient( response.ensureSuccess("mirror.scan") return response.body() } + + companion object { + /** Сколько раз повторяем [list] при сбое (один повтор на подёргивание сервера). */ + private const val LIST_ATTEMPTS = 2 + + private const val LIST_RETRY_DELAY_MS = 1_500L + } }