feat: разделение контента — на телефон только аудио, на очки только видео

Телефон качает audio-N.ogg (audioKeys вместо fileKeys, стадия «звук»),
очки получают DownloadFiles только с video.mkv (buildGlassesFileSpecs).
Watch проверяет готовность обоих: телефон — audioReady(), очки — как
было; отказ с подсказкой «Скачать на телефон/очки». Тесты: lib-core 19,
phone 36.
This commit is contained in:
Hermes Agent
2026-08-20 01:41:43 +03:00
parent 9f1c6735c1
commit 2844d6a3ac
7 changed files with 77 additions and 32 deletions
@@ -11,7 +11,8 @@ fun trackFileName(key: String): String = if (key == "video") "video.mkv" else "$
fun trackKeys(files: MirrorFiles): List<String> =
listOf("video") + files.audios.map { "audio-${it.index}" }
/** Собрать список файлов для DownloadFiles на очки: presigned URL, имя файла, размер (если известен). */
/** Собрать список файлов для DownloadFiles на очки: presigned URL, имя файла, размер (если известен).
* На очки — только video.mkv; звук — на телефоне. */
fun buildGlassesFileSpecs(
files: MirrorFiles,
sizes: Map<String, Long>,
@@ -22,15 +23,7 @@ fun buildGlassesFileSpecs(
name = trackFileName("video"),
sizeBytes = sizes["video"] ?: 0L,
)
val audios = files.audios.map { a ->
val key = "audio-${a.index}"
FileSpec(
url = presign(a.url),
name = trackFileName(key),
sizeBytes = sizes[key] ?: 0L,
)
}
return listOf(video) + audios
return listOf(video)
}
/** Полное сообщение DownloadFiles для очков. */
@@ -89,8 +89,9 @@ class PhoneActions(
/**
* Включить на очках: зеркало должно быть done → PlayVideo с ЛОКАЛЬНЫМ путём на очках.
* Стриминг запрещён: [localVideoPath] — file://... с диска очков (или пустой — не играем).
* Звук — на телефоне: [phoneAudioReady] — скачаны ли аудио-дорожки на телефон.
*/
suspend fun watch(itemId: String, audioIndex: Int = 0, localVideoPath: String = ""): String {
suspend fun watch(itemId: String, audioIndex: Int = 0, localVideoPath: String = "", phoneAudioReady: Boolean = true): String {
val item = jellyfin.item(userId, itemId) ?: return "элемент $itemId не найден"
val status = mirror.mirrorByItem(itemId)
val files = status?.files
@@ -100,6 +101,9 @@ class PhoneActions(
if (!localVideoPath.startsWith("file://")) {
return "«${item.Name ?: itemId}» не скачан на очки — сначала «Скачать на очки»"
}
if (!phoneAudioReady) {
return "«${item.Name ?: itemId}» не скачан на телефон (звук) — сначала «Скачать на телефон»"
}
val title = item.Name ?: itemId
sender.send(PlayVideo(itemId = itemId, title = title, videoUrl = localVideoPath, audioIndex = audioIndex))
@@ -187,6 +187,17 @@ class PhoneActionsTest {
assertTrue(sender.sent.isEmpty())
}
@Test
fun watchWhenPhoneAudioNotReadyIsRejected() = runTest {
val sender = SenderStub()
val out = actions(simpleJellyfin(), doneMirror(), sender)
.watch("m1", localVideoPath = "file:///x/m1/video.mkv", phoneAudioReady = false)
assertTrue(out.contains("не скачан на телефон"))
assertTrue(out.contains("Скачать на телефон"))
assertTrue(sender.sent.isEmpty())
}
@Test
fun watchWhenMirrorNotReadyReturnsHint() = runTest {
val sender = SenderStub()
@@ -295,7 +306,7 @@ class PhoneActionsTest {
assertIs<DownloadFiles>(msg)
assertEquals("m1", msg.itemId)
assertEquals("Интерстеллар", msg.title)
assertEquals(listOf("video.mkv", "audio-1.ogg", "audio-2.ogg"), msg.files.map { it.name })
assertEquals(listOf("video.mkv"), msg.files.map { it.name })
assertEquals("signed-https://s3/media/m1/video.mp4", msg.files[0].url)
assertEquals(100L, msg.files[0].sizeBytes)
assertTrue(msg.files[0].name.endsWith(".mkv"))