feat: разделение контента — на телефон только аудио, на очки только видео
Телефон качает audio-N.ogg (audioKeys вместо fileKeys, стадия «звук»), очки получают DownloadFiles только с video.mkv (buildGlassesFileSpecs). Watch проверяет готовность обоих: телефон — audioReady(), очки — как было; отказ с подсказкой «Скачать на телефон/очки». Тесты: lib-core 19, phone 36.
This commit is contained in:
+3
-10
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user