fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления
- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео - resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null) - DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job) - Тесты 62/0 (в т.ч. поколения — поймали null != 0L) Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package pw.binom.viewmate.core.phone
|
||||
|
||||
import pw.binom.viewmate.core.media.MirrorFiles
|
||||
import pw.binom.viewmate.core.protocol.DownloadFiles
|
||||
import pw.binom.viewmate.core.protocol.FileSpec
|
||||
|
||||
/** Имя файла для ключа дорожки ("video" → "video.mkv", "audio-0" → "audio-0.ogg"). */
|
||||
fun trackFileName(key: String): String = if (key == "video") "video.mkv" else "$key.ogg"
|
||||
|
||||
/** Ключи дорожек зеркала: video + каждая аудио (из files.audios, а не с диска). */
|
||||
fun trackKeys(files: MirrorFiles): List<String> =
|
||||
listOf("video") + files.audios.map { "audio-${it.index}" }
|
||||
|
||||
/** Собрать список файлов для DownloadFiles на очки: presigned URL, имя файла, размер (если известен). */
|
||||
fun buildGlassesFileSpecs(
|
||||
files: MirrorFiles,
|
||||
sizes: Map<String, Long>,
|
||||
presign: (String) -> String,
|
||||
): List<FileSpec> {
|
||||
val video = FileSpec(
|
||||
url = presign(files.video.url),
|
||||
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
|
||||
}
|
||||
|
||||
/** Полное сообщение DownloadFiles для очков. */
|
||||
fun buildDownloadFiles(
|
||||
itemId: String,
|
||||
title: String,
|
||||
files: MirrorFiles,
|
||||
sizes: Map<String, Long>,
|
||||
presign: (String) -> String,
|
||||
): DownloadFiles = DownloadFiles(
|
||||
itemId = itemId,
|
||||
title = title,
|
||||
files = buildGlassesFileSpecs(files, sizes, presign),
|
||||
)
|
||||
@@ -11,6 +11,8 @@ import pw.binom.viewmate.core.media.MirrorStatus
|
||||
import pw.binom.viewmate.core.protocol.AssistantState
|
||||
import pw.binom.viewmate.core.protocol.AssistantStateMsg
|
||||
import pw.binom.viewmate.core.protocol.ChatSessionMsg
|
||||
import pw.binom.viewmate.core.protocol.DeleteFiles
|
||||
import pw.binom.viewmate.core.protocol.DownloadFiles
|
||||
import pw.binom.viewmate.core.protocol.MediaCommandMsg
|
||||
import pw.binom.viewmate.core.protocol.PlayVideo
|
||||
import pw.binom.viewmate.core.protocol.SetMode
|
||||
@@ -83,25 +85,58 @@ class PhoneActions(
|
||||
|
||||
// --- включить на очках ---
|
||||
|
||||
/** Включить на очках: зеркало должно быть done → PlayVideo с audioIndex. */
|
||||
suspend fun watch(itemId: String, audioIndex: Int = 0, presign: (String) -> String = { it }): String {
|
||||
/**
|
||||
* Включить на очках: зеркало должно быть done → PlayVideo с ЛОКАЛЬНЫМ путём на очках.
|
||||
* Стриминг запрещён: [localVideoPath] — file://... с диска очков (или пустой — не играем).
|
||||
*/
|
||||
suspend fun watch(itemId: String, audioIndex: Int = 0, localVideoPath: String = ""): String {
|
||||
val item = jellyfin.item(userId, itemId) ?: return "элемент $itemId не найден"
|
||||
val status = mirror.mirrorByItem(itemId)
|
||||
val files = status?.files
|
||||
if (status == null || status.status != "done" || files == null) {
|
||||
return "зеркало для «${item.Name ?: itemId}» не готово (${status?.status ?: "нет"}). Закажите: mirror $itemId"
|
||||
}
|
||||
if (!localVideoPath.startsWith("file://")) {
|
||||
return "«${item.Name ?: itemId}» не скачан на очки — сначала «Скачать на очки»"
|
||||
}
|
||||
|
||||
val title = item.Name ?: itemId
|
||||
val videoUrl = presign(files.video.url)
|
||||
sender.send(PlayVideo(itemId = itemId, title = title, videoUrl = videoUrl, audioIndex = audioIndex))
|
||||
sender.send(PlayVideo(itemId = itemId, title = title, videoUrl = localVideoPath, audioIndex = audioIndex))
|
||||
state.mode = GlassesMode.MOVIE
|
||||
state.currentItem = item
|
||||
state.currentAudioIndex = audioIndex
|
||||
state.mirrorStatus = status
|
||||
state.playing = true
|
||||
state.downloaded[itemId] = "очки"
|
||||
return "включил «$title» на очках (озвучка #$audioIndex): $videoUrl"
|
||||
return "включил «$title» на очках из локального файла (озвучка #$audioIndex)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачать контент в память очков: DownloadFiles с presigned URL всех файлов
|
||||
* (video.mkv + audio-N.ogg) и известными размерами дорожек [sizes] (key → байт).
|
||||
*/
|
||||
suspend fun downloadToGlasses(
|
||||
itemId: String,
|
||||
sizes: Map<String, Long> = emptyMap(),
|
||||
presign: (String) -> String = { it },
|
||||
): String {
|
||||
val item = jellyfin.item(userId, itemId) ?: return "элемент $itemId не найден"
|
||||
val status = mirror.mirrorByItem(itemId)
|
||||
val files = status?.files
|
||||
if (status == null || status.status != "done" || files == null) {
|
||||
return "зеркало для «${item.Name ?: itemId}» не готово (${status?.status ?: "нет"})"
|
||||
}
|
||||
val title = item.Name ?: itemId
|
||||
val msg = buildDownloadFiles(
|
||||
itemId = itemId,
|
||||
title = title,
|
||||
files = files,
|
||||
sizes = sizes,
|
||||
presign = presign,
|
||||
)
|
||||
sender.send(msg)
|
||||
log("phone", "скачивание на очки: $itemId (${msg.files.size} файлов)")
|
||||
return "загрузка «$title» на очки"
|
||||
}
|
||||
|
||||
// --- зеркала ---
|
||||
@@ -146,11 +181,12 @@ class PhoneActions(
|
||||
return "скачано $itemId → $target (эмуляция, отмечено в стейте)"
|
||||
}
|
||||
|
||||
/** Удалить скачанное (эмуляция). */
|
||||
fun deleteDownloaded(itemId: String): String {
|
||||
/** Удалить скачанное: DeleteFiles на очки (каталог контента) + снять эмуляционную пометку. */
|
||||
suspend fun deleteDownloaded(itemId: String): String {
|
||||
sender.send(DeleteFiles(itemId))
|
||||
val removed = state.downloaded.remove(itemId)
|
||||
return if (removed != null) "удалено скачанное: $itemId ($removed)"
|
||||
else "не было скачано: $itemId"
|
||||
else "не было скачано (эмуляция): $itemId"
|
||||
}
|
||||
|
||||
// --- сессии ---
|
||||
|
||||
@@ -33,11 +33,34 @@ data class MediaCommandMsg(val command: MediaCommand, val seekMs: Long = 0) : Ho
|
||||
data class PlayVideo(
|
||||
val itemId: String,
|
||||
val title: String,
|
||||
/** ЛОКАЛЬНЫЙ путь на очках (file:///...) ИЛИ пустой. Очки не стримят: http(s)-URL запрещены. */
|
||||
val videoUrl: String,
|
||||
val audioIndex: Int = 0,
|
||||
val startPositionMs: Long = 0,
|
||||
) : HostToGlasses
|
||||
|
||||
/** Файл контента для скачивания на очки. */
|
||||
@Serializable
|
||||
data class FileSpec(
|
||||
val url: String,
|
||||
val name: String,
|
||||
val sizeBytes: Long = 0,
|
||||
)
|
||||
|
||||
/** Скачать контент в память очков: каталог filesDir/viewmate/{itemId}/, файлы — по [FileSpec.name]. */
|
||||
@Serializable
|
||||
@SerialName("download_files")
|
||||
data class DownloadFiles(
|
||||
val itemId: String,
|
||||
val title: String,
|
||||
val files: List<FileSpec>,
|
||||
) : HostToGlasses
|
||||
|
||||
/** Удалить каталог контента на очках. */
|
||||
@Serializable
|
||||
@SerialName("delete_files")
|
||||
data class DeleteFiles(val itemId: String) : HostToGlasses
|
||||
|
||||
/** Состояние ассистента (IDLE/LISTENING/THINKING/SPEAKING) + время записи для панели. */
|
||||
@Serializable
|
||||
@SerialName("assistant_state")
|
||||
|
||||
@@ -22,6 +22,39 @@ data class GlassesStatus(
|
||||
val batteryPercent: Int,
|
||||
val storageUsedGb: Double,
|
||||
val storageTotalGb: Double,
|
||||
/** Полный объём встроенной памяти, байты. */
|
||||
val storageTotalBytes: Long = 0,
|
||||
/** Свободно байт. */
|
||||
val storageFreeBytes: Long = 0,
|
||||
/** Сумма скачанного медиа (filesDir/viewmate), байты. */
|
||||
val mediaBytes: Long = 0,
|
||||
/** itemId полностью скачанных на очки контентов. */
|
||||
val downloadedItemIds: List<String> = emptyList(),
|
||||
/** Каталог контента на очках (filesDir/viewmate) — для локального пути file:// в PlayVideo. */
|
||||
val mediaDir: String = "",
|
||||
/** Версия приложения очков (для экрана информации). */
|
||||
val appVersion: String = "",
|
||||
) : GlassesToHost
|
||||
|
||||
/** Фаза скачивания на очки. */
|
||||
@Serializable
|
||||
enum class DownloadPhase {
|
||||
DOWNLOADING,
|
||||
DONE,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
/** Прогресс скачивания одного файла на очки (очки → телефон). */
|
||||
@Serializable
|
||||
@SerialName("download_progress")
|
||||
data class DownloadProgress(
|
||||
val itemId: String,
|
||||
/** Имя файла ("video.mkv", "audio-0.ogg") — по нему телефон размечает строки файлов. */
|
||||
val fileName: String,
|
||||
val percent: Int,
|
||||
val doneBytes: Long,
|
||||
val totalBytes: Long,
|
||||
val phase: DownloadPhase,
|
||||
) : GlassesToHost
|
||||
|
||||
/** Позиция воспроизведения (очки — мастер таймкода, шлют регулярно). */
|
||||
|
||||
@@ -19,6 +19,8 @@ import pw.binom.viewmate.core.media.CreateMirrorRequest
|
||||
import pw.binom.viewmate.core.media.JellyfinClient
|
||||
import pw.binom.viewmate.core.media.MirrorClient
|
||||
import pw.binom.viewmate.core.protocol.ChatSessionMsg
|
||||
import pw.binom.viewmate.core.protocol.DeleteFiles
|
||||
import pw.binom.viewmate.core.protocol.DownloadFiles
|
||||
import pw.binom.viewmate.core.protocol.HostToGlasses
|
||||
import pw.binom.viewmate.core.protocol.PlayVideo
|
||||
import kotlin.test.Test
|
||||
@@ -156,9 +158,10 @@ class PhoneActionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun watchSendsPlayVideoWithUrlAndAudioIndex() = runTest {
|
||||
fun watchSendsPlayVideoWithLocalPathAndAudioIndex() = runTest {
|
||||
val sender = SenderStub()
|
||||
val out = actions(simpleJellyfin(), doneMirror(), sender).watch("m1", audioIndex = 1)
|
||||
val out = actions(simpleJellyfin(), doneMirror(), sender)
|
||||
.watch("m1", audioIndex = 1, localVideoPath = "file:///data/media/viewmate/m1/video.mkv")
|
||||
|
||||
assertTrue(out.contains("включил «Интерстеллар»"))
|
||||
assertEquals(1, sender.sent.size)
|
||||
@@ -166,7 +169,7 @@ class PhoneActionsTest {
|
||||
assertIs<PlayVideo>(msg)
|
||||
assertEquals("m1", msg.itemId)
|
||||
assertEquals("Интерстеллар", msg.title)
|
||||
assertEquals("https://s3/media/m1/video.mp4", msg.videoUrl)
|
||||
assertEquals("file:///data/media/viewmate/m1/video.mkv", msg.videoUrl)
|
||||
assertEquals(1, msg.audioIndex)
|
||||
|
||||
assertEquals("m1", sender.phoneState.currentItem?.Id)
|
||||
@@ -175,13 +178,22 @@ class PhoneActionsTest {
|
||||
assertEquals("done", sender.phoneState.mirrorStatus?.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun watchWithoutLocalPathIsRejected() = runTest {
|
||||
val sender = SenderStub()
|
||||
val out = actions(simpleJellyfin(), doneMirror(), sender).watch("m1")
|
||||
|
||||
assertTrue(out.contains("не скачан на очки"))
|
||||
assertTrue(sender.sent.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun watchWhenMirrorNotReadyReturnsHint() = runTest {
|
||||
val sender = SenderStub()
|
||||
val mirrorClient = mirror { request ->
|
||||
respond("", HttpStatusCode.NotFound)
|
||||
}
|
||||
val out = actions(simpleJellyfin(), mirrorClient, sender).watch("m1")
|
||||
val out = actions(simpleJellyfin(), mirrorClient, sender).watch("m1", localVideoPath = "file:///x/m1/video.mkv")
|
||||
|
||||
assertTrue(out.contains("не готово"))
|
||||
assertTrue(sender.sent.isEmpty())
|
||||
@@ -272,7 +284,35 @@ class PhoneActionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadAndDeleteDownloadedEmulate() {
|
||||
fun downloadToGlassesBuildsAndSendsDownloadFiles() = runTest {
|
||||
val sender = SenderStub()
|
||||
val out = actions(simpleJellyfin(), doneMirror(), sender)
|
||||
.downloadToGlasses("m1", sizes = mapOf("video" to 100L)) { "signed-$it" }
|
||||
|
||||
assertTrue(out.contains("загрузка"))
|
||||
assertEquals(1, sender.sent.size)
|
||||
val msg = sender.sent.single()
|
||||
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("signed-https://s3/media/m1/video.mp4", msg.files[0].url)
|
||||
assertEquals(100L, msg.files[0].sizeBytes)
|
||||
assertTrue(msg.files[0].name.endsWith(".mkv"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadToGlassesWhenMirrorNotReadyReturnsHint() = runTest {
|
||||
val sender = SenderStub()
|
||||
val mirrorClient = mirror { request -> respond("", HttpStatusCode.NotFound) }
|
||||
val out = actions(simpleJellyfin(), mirrorClient, sender).downloadToGlasses("m1")
|
||||
|
||||
assertTrue(out.contains("не готово"))
|
||||
assertTrue(sender.sent.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadAndDeleteDownloadedEmulate() = runTest {
|
||||
val sender = SenderStub()
|
||||
val actions = actions(simpleJellyfin(), doneMirror(), sender)
|
||||
|
||||
@@ -288,6 +328,17 @@ class PhoneActionsTest {
|
||||
assertTrue(rm2.contains("не было скачано"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteDownloadedSendsDeleteFilesToGlasses() = runTest {
|
||||
val sender = SenderStub()
|
||||
actions(simpleJellyfin(), doneMirror(), sender).deleteDownloaded("m1")
|
||||
|
||||
assertEquals(1, sender.sent.size)
|
||||
val msg = sender.sent.single()
|
||||
assertIs<DeleteFiles>(msg)
|
||||
assertEquals("m1", msg.itemId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statusShowsCurrentState() = runTest {
|
||||
val sender = SenderStub()
|
||||
@@ -298,7 +349,7 @@ class PhoneActionsTest {
|
||||
assertTrue(empty.contains("ничего"))
|
||||
assertTrue(empty.contains("пауза"))
|
||||
|
||||
actions.watch("m1")
|
||||
actions.watch("m1", localVideoPath = "file:///x/m1/video.mkv")
|
||||
val after = actions.status()
|
||||
assertTrue(after.contains("Играет: Интерстеллар (m1)"))
|
||||
assertTrue(after.contains("играет"))
|
||||
|
||||
@@ -37,6 +37,77 @@ class GlassesToHostTest {
|
||||
assertEquals(32.0, decoded.storageTotalGb)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesStatusExtendedFieldsRoundTrip() {
|
||||
val msg: GlassesToHost = GlassesStatus(
|
||||
batteryPercent = 60,
|
||||
storageUsedGb = 12.5,
|
||||
storageTotalGb = 32.0,
|
||||
storageTotalBytes = 34_359_738_368,
|
||||
storageFreeBytes = 20_971_520_000,
|
||||
mediaBytes = 1_863_946_240,
|
||||
downloadedItemIds = listOf("i1", "i2"),
|
||||
mediaDir = "/data/user/0/pw.binom.viewmate.glasses/files/viewmate",
|
||||
appVersion = "0.2.0",
|
||||
)
|
||||
val text = json.encodeToString(GlassesToHost.serializer(), msg)
|
||||
val decoded = json.decodeFromString(GlassesToHost.serializer(), text)
|
||||
assertIs<GlassesStatus>(decoded)
|
||||
assertEquals(60, decoded.batteryPercent)
|
||||
assertEquals(34_359_738_368, decoded.storageTotalBytes)
|
||||
assertEquals(20_971_520_000, decoded.storageFreeBytes)
|
||||
assertEquals(1_863_946_240, decoded.mediaBytes)
|
||||
assertEquals(listOf("i1", "i2"), decoded.downloadedItemIds)
|
||||
assertEquals("/data/user/0/pw.binom.viewmate.glasses/files/viewmate", decoded.mediaDir)
|
||||
assertEquals("0.2.0", decoded.appVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun glassesStatusDefaultsForOldFields() {
|
||||
val msg: GlassesToHost = GlassesStatus(batteryPercent = 70, storageUsedGb = 10.5, storageTotalGb = 32.0)
|
||||
val decoded = json.decodeFromString(GlassesToHost.serializer(), json.encodeToString(GlassesToHost.serializer(), msg))
|
||||
assertIs<GlassesStatus>(decoded)
|
||||
assertEquals(0, decoded.storageTotalBytes)
|
||||
assertEquals(0, decoded.storageFreeBytes)
|
||||
assertEquals(0, decoded.mediaBytes)
|
||||
assertTrue(decoded.downloadedItemIds.isEmpty())
|
||||
assertEquals("", decoded.mediaDir)
|
||||
assertEquals("", decoded.appVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadProgressRoundTrip() {
|
||||
val msg: GlassesToHost = DownloadProgress(
|
||||
itemId = "movie-1",
|
||||
fileName = "video.mkv",
|
||||
percent = 45,
|
||||
doneBytes = 838_860_800,
|
||||
totalBytes = 1_863_946_240,
|
||||
phase = DownloadPhase.DOWNLOADING,
|
||||
)
|
||||
val text = json.encodeToString(GlassesToHost.serializer(), msg)
|
||||
val decoded = json.decodeFromString(GlassesToHost.serializer(), text)
|
||||
assertIs<DownloadProgress>(decoded)
|
||||
assertEquals("movie-1", decoded.itemId)
|
||||
assertEquals("video.mkv", decoded.fileName)
|
||||
assertEquals(45, decoded.percent)
|
||||
assertEquals(838_860_800, decoded.doneBytes)
|
||||
assertEquals(1_863_946_240, decoded.totalBytes)
|
||||
assertEquals(DownloadPhase.DOWNLOADING, decoded.phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadProgressDoneAndFailedPhases() {
|
||||
val done: GlassesToHost = DownloadProgress("i", "video.mkv", 100, 10, 10, DownloadPhase.DONE)
|
||||
val failed: GlassesToHost = DownloadProgress("i", "audio-0.ogg", 0, 5, 10, DownloadPhase.FAILED)
|
||||
val decodedDone = json.decodeFromString(GlassesToHost.serializer(), json.encodeToString(GlassesToHost.serializer(), done))
|
||||
val decodedFailed = json.decodeFromString(GlassesToHost.serializer(), json.encodeToString(GlassesToHost.serializer(), failed))
|
||||
assertIs<DownloadProgress>(decodedDone)
|
||||
assertEquals(DownloadPhase.DONE, decodedDone.phase)
|
||||
assertIs<DownloadProgress>(decodedFailed)
|
||||
assertEquals(DownloadPhase.FAILED, decodedFailed.phase)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playbackPositionRoundTrip() {
|
||||
val msg: GlassesToHost = PlaybackPosition(positionMs = 65_432, playing = true)
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class HostToGlassesTest {
|
||||
|
||||
@@ -113,6 +114,57 @@ class HostToGlassesTest {
|
||||
assertEquals("Планы на выходные", decoded.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadFilesRoundTrip() {
|
||||
val msg: HostToGlasses = DownloadFiles(
|
||||
itemId = "movie-1",
|
||||
title = "Interstellar",
|
||||
files = listOf(
|
||||
FileSpec(url = "https://s3/mirror/video.mkv?sig", name = "video.mkv", sizeBytes = 1_863_946_240),
|
||||
FileSpec(url = "https://s3/mirror/audio-0.ogg?sig", name = "audio-0.ogg", sizeBytes = 68_157_440),
|
||||
),
|
||||
)
|
||||
val text = json.encodeToString(HostToGlasses.serializer(), msg)
|
||||
val decoded = json.decodeFromString(HostToGlasses.serializer(), text)
|
||||
assertIs<DownloadFiles>(decoded)
|
||||
assertEquals("movie-1", decoded.itemId)
|
||||
assertEquals("Interstellar", decoded.title)
|
||||
assertEquals(2, decoded.files.size)
|
||||
assertEquals("video.mkv", decoded.files[0].name)
|
||||
assertEquals(1_863_946_240, decoded.files[0].sizeBytes)
|
||||
assertTrue(decoded.files[0].url.startsWith("https://s3/mirror/video.mkv"))
|
||||
assertEquals("audio-0.ogg", decoded.files[1].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadFilesDefaultSize() {
|
||||
val msg: HostToGlasses = DownloadFiles(
|
||||
itemId = "i",
|
||||
title = "t",
|
||||
files = listOf(FileSpec(url = "https://s3/f", name = "video.mkv")),
|
||||
)
|
||||
val decoded = json.decodeFromString(HostToGlasses.serializer(), json.encodeToString(HostToGlasses.serializer(), msg))
|
||||
assertIs<DownloadFiles>(decoded)
|
||||
assertEquals(0, decoded.files[0].sizeBytes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteFilesRoundTrip() {
|
||||
val msg: HostToGlasses = DeleteFiles(itemId = "movie-1")
|
||||
val text = json.encodeToString(HostToGlasses.serializer(), msg)
|
||||
val decoded = json.decodeFromString(HostToGlasses.serializer(), text)
|
||||
assertIs<DeleteFiles>(decoded)
|
||||
assertEquals("movie-1", decoded.itemId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playVideoEmptyVideoUrlAllowed() {
|
||||
val msg: HostToGlasses = PlayVideo(itemId = "i", title = "t", videoUrl = "")
|
||||
val decoded = json.decodeFromString(HostToGlasses.serializer(), json.encodeToString(HostToGlasses.serializer(), msg))
|
||||
assertIs<PlayVideo>(decoded)
|
||||
assertEquals("", decoded.videoUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discriminatorIsType() {
|
||||
val text = json.encodeToString(HostToGlasses.serializer(), SetMode(GlassesMode.CHAT))
|
||||
|
||||
Reference in New Issue
Block a user