feat: разделение контента — на телефон только аудио, на очки только видео
Телефон качает audio-N.ogg (audioKeys вместо fileKeys, стадия «звук»), очки получают DownloadFiles только с video.mkv (buildGlassesFileSpecs). Watch проверяет готовность обоих: телефон — audioReady(), очки — как было; отказ с подсказкой «Скачать на телефон/очки». Тесты: lib-core 19, phone 36.
This commit is contained in:
@@ -125,6 +125,10 @@ internal fun fileKeyFor(partName: String): String {
|
||||
internal fun fileKeys(files: MirrorFiles): List<String> =
|
||||
listOf("video") + files.audios.map { "audio-${it.index}" }
|
||||
|
||||
/** Ключи аудио-дорожек зеркала (audio-N) — телефон качает только звук. */
|
||||
internal fun audioKeys(files: MirrorFiles): List<String> =
|
||||
files.audios.map { "audio-${it.index}" }
|
||||
|
||||
/** Полностью ли скачано на диск: все финальные файлы [keys] есть, >0 байт, без .part. */
|
||||
internal fun isFullyOnDisk(dir: File, keys: List<String>): Boolean {
|
||||
if (keys.isEmpty()) return false
|
||||
@@ -205,7 +209,7 @@ internal fun scanDownloads(
|
||||
val biggest = parts.maxByOrNull { it.length() } ?: return@forEach
|
||||
if (total > 0) ((biggest.length() * 100) / total).toInt().coerceIn(0, 99) else 0
|
||||
}
|
||||
val stage = savedStatus?.stage?.takeIf { it.isNotBlank() } ?: "видео"
|
||||
val stage = savedStatus?.stage?.takeIf { it.isNotBlank() } ?: "звук"
|
||||
progress[itemId] = DownloadProgress(itemId, DownloadPhase.DOWNLOADING, percent, stage)
|
||||
}
|
||||
}
|
||||
@@ -221,8 +225,8 @@ internal fun requestFor(url: String, existingBytes: Long): Request {
|
||||
}
|
||||
|
||||
/**
|
||||
* Реальное скачивание зеркала (видео + все аудио-дорожки) с общим прогрессом.
|
||||
* Файлы: filesDir/viewmate/{itemId}/video.mkv + audio-{index}.ogg.
|
||||
* Реальное скачивание зеркала (только аудио-дорожки — звук на телефоне) с общим прогрессом.
|
||||
* Файлы: filesDir/viewmate/{itemId}/audio-{index}.ogg.
|
||||
* Скачивание пишется в .part и переживает рестарт: статус хранится в status.json,
|
||||
* при старте диск сканируется, докачка — только по явному тапу пользователя.
|
||||
*/
|
||||
@@ -251,7 +255,7 @@ class DownloadManager(
|
||||
val downloads: StateFlow<Map<String, DownloadProgress>> = _downloads.asStateFlow()
|
||||
|
||||
private val _downloadedIds = MutableStateFlow<Set<String>>(emptySet())
|
||||
/** itemId, у которых видео уже скачано на телефон (переживает рестарт приложения). */
|
||||
/** itemId, у которых звук уже скачан на телефон (переживает рестарт приложения). */
|
||||
val downloadedIds: StateFlow<Set<String>> = _downloadedIds.asStateFlow()
|
||||
|
||||
private val _activeIds = MutableStateFlow<Set<String>>(emptySet())
|
||||
@@ -321,15 +325,25 @@ class DownloadManager(
|
||||
fun isDownloaded(itemId: String): Boolean = videoFile(itemId).exists()
|
||||
|
||||
/**
|
||||
* Полностью ли скачано зеркало: video.mkv + все аудио из [files] (без .part, >0 байт).
|
||||
* Полностью ли скачан звук зеркала на телефон: все аудио из [files] (без .part, >0 байт).
|
||||
* Список дорожек берётся из зеркала (files.audios), а не из того, что есть на диске.
|
||||
*/
|
||||
fun isFullyDownloaded(itemId: String, files: MirrorFiles?): Boolean {
|
||||
val keys = files?.let { fileKeys(it) } ?: return false
|
||||
val keys = files?.let { audioKeys(it) } ?: return false
|
||||
if (keys.isEmpty()) return false
|
||||
return isFullyOnDisk(dirFor(itemId), keys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Все аудио-дорожки зеркала на диске (можно смотреть: звук будет).
|
||||
* Список дорожек — из статуса качки (expectedFiles), а не с диска.
|
||||
*/
|
||||
fun audioReady(itemId: String): Boolean {
|
||||
val status = statuses[itemId] ?: return false
|
||||
if (status.expectedFiles.isEmpty()) return false
|
||||
return isFullyOnDisk(dirFor(itemId), status.expectedFiles)
|
||||
}
|
||||
|
||||
/** Общий процент скачанного на диск по всем дорожкам (по .part и полным файлам). */
|
||||
fun overallPercent(itemId: String): Int {
|
||||
val st = statuses[itemId] ?: return 0
|
||||
@@ -352,7 +366,7 @@ class DownloadManager(
|
||||
percentForFile(itemId, part.name.removeSuffix(".part")) ?: 0
|
||||
|
||||
/**
|
||||
* Начать (или продолжить) загрузку зеркала [itemId]: видео + ВСЕ аудио-дорожки.
|
||||
* Начать (или продолжить) загрузку зеркала [itemId]: ТОЛЬКО аудио-дорожки (звук на телефоне).
|
||||
* Если уже качается — повторный вызов игнорируется; если всё полностью скачано — тоже.
|
||||
* При наличии .part докачивает с места обрыва (Range: bytes={размер}-) для каждого файла.
|
||||
*/
|
||||
@@ -376,7 +390,7 @@ class DownloadManager(
|
||||
if (!stale()) update(itemId, DownloadProgress(itemId, DownloadPhase.FAILED, 0, "зеркало не готово"))
|
||||
return@launch
|
||||
}
|
||||
val keys = fileKeys(files)
|
||||
val keys = audioKeys(files)
|
||||
val dir = dirFor(itemId)
|
||||
if (stale()) return@launch
|
||||
dir.mkdirs()
|
||||
@@ -393,9 +407,8 @@ class DownloadManager(
|
||||
val done = HashMap<String, Long>()
|
||||
keys.forEach { key ->
|
||||
if (stale()) return@launch
|
||||
val url = if (key == "video") files.video.url
|
||||
else files.audios.firstOrNull { "audio-${it.index}" == key }?.url ?: return@forEach
|
||||
val stage = if (key == "video") "видео" else "звук"
|
||||
val url = files.audios.firstOrNull { "audio-${it.index}" == key }?.url ?: return@forEach
|
||||
val stage = "звук"
|
||||
downloadFile(
|
||||
url = S3Presigner.presignGet(url, PhoneConfig.S3_ACCESS_KEY, PhoneConfig.S3_SECRET_KEY, PhoneConfig.S3_REGION),
|
||||
part = File(dir, partFileName(key)),
|
||||
|
||||
@@ -150,6 +150,12 @@ fun DetailsScreen(
|
||||
scope.launch { snackbar.showSnackbar("сначала нажми «Скачать на очки»") }
|
||||
return
|
||||
}
|
||||
val audioOk = dm.audioReady(item.Id)
|
||||
if (!audioOk) {
|
||||
log("watch", "пропущено: звук не скачан на телефон (audioReady=false)")
|
||||
scope.launch { snackbar.showSnackbar("сначала нажми «Скачать на телефон»") }
|
||||
return
|
||||
}
|
||||
if (glassesMediaDir.isNullOrBlank()) {
|
||||
log("watch", "пропущено: очки ещё не сообщили каталог (mediaDir пуст)")
|
||||
scope.launch { snackbar.showSnackbar("очки ещё не сообщили каталог — подожди") }
|
||||
@@ -179,7 +185,7 @@ fun DetailsScreen(
|
||||
log("watch", "видео уже играет на очках — PlayVideo не отправляю, запускаю только звук")
|
||||
} else {
|
||||
result = try {
|
||||
actions!!.watch(item.Id, effectiveAudioIndex, localVideoPath = localPath)
|
||||
actions!!.watch(item.Id, effectiveAudioIndex, localVideoPath = localPath, phoneAudioReady = audioOk)
|
||||
} catch (e: Exception) {
|
||||
log("watch", "watch упал: ${e.message}")
|
||||
snackbar.showSnackbar("watch упал: ${e.message}")
|
||||
|
||||
Reference in New Issue
Block a user