feat: app-host → телефон-эмулятор (Jellyfin + mirror + стейт + команды)

- Config.kt: env > application.properties > дефолты (креды не в коде)
- PhoneState.kt: единый стейт — mode, currentItem, audioIndex, mirrorStatus, activeSessionId, сессии (эмуляция), downloaded
- PhoneActions.kt: catalog (с бейджами зеркал), open, watch (PlayVideo на очки), makeMirror, mirrorStatus, download/delete, sessions/selectSession, status
- Main.kt: новые консольные команды (catalog/open/watch/mirror/dl/rm/sessions/session/status)
- GlassesHub: Welcome берёт mode/sessionId из стейта, glasses_off → пауза
- JellyfinClient.item() — элемент по id (для open/watch)
- 58 тестов, 130 всего, все зелёные; живые проверки: catalog (реальный Jellyfin), watch (PlayVideo с s3-url), status (стейт обновляется)
This commit is contained in:
Hermes Agent
2026-08-11 14:37:43 +03:00
parent d95d289d29
commit f0f1f71575
11 changed files with 879 additions and 25 deletions
@@ -5,6 +5,7 @@ import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.http.HttpStatusCode
/**
* HTTP-клиент Jellyfin (каталог).
@@ -37,6 +38,17 @@ class JellyfinClient(
recursive = false,
)
/** Один элемент по id; null если не найден (404). */
suspend fun item(userId: String, itemId: String): JellyfinItem? {
val response = http.get("$baseUrl/Users/$userId/Items/$itemId") {
header("X-Emby-Token", apiKey)
parameter("Fields", "PrimaryImageAspectRatio,Overview")
}
if (response.status == HttpStatusCode.NotFound) return null
response.ensureSuccess("jellyfin.item")
return response.body()
}
/** Поиск по всей библиотеке. */
suspend fun search(userId: String, query: String): List<JellyfinItem> = requestItems(
userId = userId,
@@ -109,6 +109,41 @@ class JellyfinClientTest {
assertEquals("Movie", items[0].Type)
}
@Test
fun itemParsesSingleItemAndSendsToken() = runTest {
var capturedPath: String? = null
var capturedToken: String? = null
val jf = JellyfinClient(
baseUrl = "https://jellyfin.binom.pw",
apiKey = apiKey,
http = client { request ->
capturedPath = request.url.encodedPath
capturedToken = request.headers["X-Emby-Token"]
jsonResponse("""{"Id":"m1","Name":"Interstellar","Type":"Movie","Overview":"Описание"}""")
},
)
val item = jf.item(userId = "u1", itemId = "m1")
assertEquals("/Users/u1/Items/m1", capturedPath)
assertEquals(apiKey, capturedToken)
kotlin.test.assertNotNull(item)
assertEquals("m1", item.Id)
assertEquals("Interstellar", item.Name)
assertEquals("Описание", item.Overview)
}
@Test
fun itemReturnsNullOnNotFound() = runTest {
val jf = JellyfinClient(
baseUrl = "https://jellyfin.binom.pw",
apiKey = apiKey,
http = client { respond("", HttpStatusCode.NotFound) },
)
kotlin.test.assertNull(jf.item(userId = "u1", itemId = "missing"))
}
@Test
fun searchSendsSearchTerm() = runTest {
var capturedSearchTerm: String? = null