From aaedb7df05fea21613865fd56a897a793c486375 Mon Sep 17 00:00:00 2001 From: Arumugam J Date: Sun, 19 Jan 2025 20:27:42 -0800 Subject: [PATCH] UI load only file names. - Added individual call for loading each file. --- .../src/commonMain/kotlin/services/Koin.kt | 11 +- .../src/commonMain/kotlin/ui/MainScreen.kt | 61 ++++++++--- .../commonMain/kotlin/ui/home/FileDetails.kt | 101 +++++++++++------- .../commonMain/kotlin/ui/home/HomeScreen.kt | 64 +++++------ .../kotlin/viewmodels/AppViewModel.kt | 28 +++++ .../kotlin/viewmodels/VideoListViewModel.kt | 26 ++++- composeApp/src/desktopMain/kotlin/Previews.kt | 18 ---- .../github/jsixface/codexvert/api/VideoApi.kt | 29 ++++- .../jsixface/codexvert/route/VideoRoutes.kt | 10 +- .../kotlin/io/github/jsixface/common/Api.kt | 9 +- .../io/github/jsixface/common/MediaModels.kt | 7 ++ 11 files changed, 246 insertions(+), 118 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/viewmodels/AppViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/services/Koin.kt b/composeApp/src/commonMain/kotlin/services/Koin.kt index 953f090..6afae37 100644 --- a/composeApp/src/commonMain/kotlin/services/Koin.kt +++ b/composeApp/src/commonMain/kotlin/services/Koin.kt @@ -12,7 +12,9 @@ import io.ktor.client.plugins.resources.Resources import io.ktor.serialization.kotlinx.cbor.cbor import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.cbor.Cbor +import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module +import viewmodels.AppViewModel import viewmodels.BackupScreenViewModel import viewmodels.JobsScreenModel import viewmodels.SettingsScreenModel @@ -22,10 +24,11 @@ object Koin { val services = module { single { createHttpClient(enableNetworkLogs = true) } - factory { SettingsScreenModel(client = get()) } - factory { JobsScreenModel(client = get()) } - factory { BackupScreenViewModel(client = get()) } - factory { VideoListViewModel(client = get()) } + factoryOf(::AppViewModel) + factoryOf(::BackupScreenViewModel) + factoryOf(::JobsScreenModel) + factoryOf(::SettingsScreenModel) + factoryOf(::VideoListViewModel) } @OptIn(ExperimentalSerializationApi::class) diff --git a/composeApp/src/commonMain/kotlin/ui/MainScreen.kt b/composeApp/src/commonMain/kotlin/ui/MainScreen.kt index e7c709d..7a35a8a 100644 --- a/composeApp/src/commonMain/kotlin/ui/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/MainScreen.kt @@ -3,16 +3,23 @@ package ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import io.github.jsixface.common.CodecsCollection +import org.koin.compose.koinInject import ui.home.HomeScreen import ui.model.AppPages +import ui.model.ModelState +import viewmodels.AppViewModel @Composable @@ -20,25 +27,49 @@ fun MainScreen() { Box(modifier = Modifier.fillMaxSize()) { var currentPage by rememberSaveable { mutableStateOf(AppPages.HOME) } + var codecsCollection by remember { mutableStateOf(null) } + val viewModel = koinInject() + var errorLoading by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + viewModel.codecsCollection.collect { + when (it) { + is ModelState.Init -> { + errorLoading = false + } - NavigationSuiteScaffold( - navigationSuiteItems = { - AppPages.entries.forEach { page -> - item( - icon = { Icon(page.icon, contentDescription = page.title) }, - onClick = { currentPage = page }, - label = { Text(page.title) }, - selected = currentPage == page - ) + is ModelState.Error -> { + errorLoading = true + } + + is ModelState.Success -> { + errorLoading = false + codecsCollection = it.result + } } } - ) { - when (currentPage) { - AppPages.HOME -> HomeScreen() - AppPages.JOBS -> JobsScreen() - AppPages.BACKUPS -> BackupsScreen() - AppPages.SETTINGS -> SettingsScreen() + } + if (errorLoading) { + Text("Error Loading!!!", style = MaterialTheme.typography.headlineSmall) + } else { + NavigationSuiteScaffold( + navigationSuiteItems = { + AppPages.entries.forEach { page -> + item( + icon = { Icon(page.icon, contentDescription = page.title) }, + onClick = { currentPage = page }, + label = { Text(page.title) }, + selected = currentPage == page + ) + } + } + ) { + when (currentPage) { + AppPages.HOME -> HomeScreen(codecsCollection) + AppPages.JOBS -> JobsScreen() + AppPages.BACKUPS -> BackupsScreen() + AppPages.SETTINGS -> SettingsScreen() + } } } } diff --git a/composeApp/src/commonMain/kotlin/ui/home/FileDetails.kt b/composeApp/src/commonMain/kotlin/ui/home/FileDetails.kt index d2a63d5..d823fbe 100644 --- a/composeApp/src/commonMain/kotlin/ui/home/FileDetails.kt +++ b/composeApp/src/commonMain/kotlin/ui/home/FileDetails.kt @@ -1,37 +1,62 @@ package ui.home -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import io.github.jsixface.common.* +import io.github.jsixface.common.Codec +import io.github.jsixface.common.Conversion import io.github.jsixface.common.Conversion.Convert +import io.github.jsixface.common.MediaTrack +import io.github.jsixface.common.VideoFile +import org.koin.compose.koinInject +import ui.model.ModelState +import viewmodels.VideoListViewModel @Composable -fun FileDetailsDialog(file: VideoFile, onDismiss: (Map?) -> Unit) { - Dialog( - onDismissRequest = { onDismiss(null) }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - FileDetails(file) { onDismiss(it) } - } -} +fun FileDetails(file: String, onDismiss: (Map?) -> Unit) { + val viewModel = koinInject() + var videoFile by remember { mutableStateOf(null) } + var errorLoading by remember { mutableStateOf(null) } + val conversion = remember { mutableStateMapOf() } + LaunchedEffect(file) { + viewModel.getVideoFile(file).collect { + when (it) { + is ModelState.Success -> { + videoFile = it.result + it.result.audios.forEach { a -> conversion[a] = Conversion.Copy } + it.result.videos.forEach { v -> conversion[v] = Conversion.Copy } + } -@Composable -fun FileDetails(file: VideoFile, onDismiss: (Map?) -> Unit) { - val conversion = remember { - mutableStateMapOf().apply { - file.audios.forEach { put(it, Conversion.Copy) } - file.videos.forEach { put(it, Conversion.Copy) } + is ModelState.Error<*> -> { + errorLoading = it.msg + } + + is ModelState.Init<*> -> { + videoFile = null + } + } } } @@ -45,33 +70,37 @@ fun FileDetails(file: VideoFile, onDismiss: (Map?) -> Un verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - Row { - Text(file.fileName, style = MaterialTheme.typography.displaySmall, modifier = padder) - } + videoFile?.let { file -> + Row { + Text(file.fileName, style = MaterialTheme.typography.displaySmall, modifier = padder) + } - if (file.audios.isNotEmpty()) Text("Audio Tracks", style = MaterialTheme.typography.bodyLarge) - LazyColumn { - itemsIndexed(file.audios) { i, track -> - CodecRow(i, track, conversion[track] ?: Conversion.Copy) { conversion[track] = it } + if (file.audios.isNotEmpty()) Text("Audio Tracks", style = MaterialTheme.typography.bodyLarge) + LazyColumn { + itemsIndexed(file.audios) { i, track -> + CodecRow(i, track, conversion[track] ?: Conversion.Copy) { conversion[track] = it } + } } - } - if (file.videos.isNotEmpty()) Text("Video Tracks", style = MaterialTheme.typography.bodyLarge) - LazyColumn { - itemsIndexed(file.videos) { i, track -> - CodecRow(i, track, conversion[track] ?: Conversion.Copy) { conversion[track] = it } + if (file.videos.isNotEmpty()) Text("Video Tracks", style = MaterialTheme.typography.bodyLarge) + LazyColumn { + itemsIndexed(file.videos) { i, track -> + CodecRow(i, track, conversion[track] ?: Conversion.Copy) { conversion[track] = it } + } } - } - Row { - Button(onClick = { onDismiss(conversion.toMap()) }, modifier = padder) { Text("Convert") } - Button(onClick = { onDismiss(null) }, modifier = padder) { Text("Cancel") } + Row { + Button(onClick = { onDismiss(conversion.toMap()) }, modifier = padder) { Text("Convert") } + Button(onClick = { onDismiss(null) }, modifier = padder) { Text("Cancel") } + } + } ?: Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("Loading...", style = MaterialTheme.typography.displayLarge, modifier = padder) + CircularProgressIndicator(modifier = padder) } } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun CodecRow(ai: Int, track: MediaTrack, selected: Conversion, onSelect: (Conversion) -> Unit) { val codecsAvailable = Codec.entries.filter { it.type == track.type } diff --git a/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt index 1d7e7f1..40989b3 100644 --- a/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -44,7 +43,7 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import getClientConfig -import io.github.jsixface.common.VideoFile +import io.github.jsixface.common.CodecsCollection import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -58,21 +57,21 @@ private val sidePad = Modifier.padding(8.dp, 0.dp, 0.dp, 0.dp) @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun HomeScreen() { +fun HomeScreen(codecsCollection: CodecsCollection?) { - val navigator = rememberListDetailPaneScaffoldNavigator() + val navigator = rememberListDetailPaneScaffoldNavigator() var loadingJob: Job? by remember { mutableStateOf(null) } var loading by remember { mutableStateOf(true) } var errorLoading by remember { mutableStateOf(false) } - var videoList by remember { mutableStateOf(listOf()) } + var videoList by remember { mutableStateOf(mapOf()) } val viewModel = koinInject() val scope = rememberCoroutineScope() - fun load() { + fun load(audioFilter: String? = null, videoFilter: String? = null) { loadingJob?.cancel() loadingJob = scope.launch { - viewModel.videoList.collect { + viewModel.videoList(audioFilter, videoFilter).collect { when (it) { is ModelState.Init -> { loading = true @@ -109,9 +108,11 @@ fun HomeScreen() { color = MaterialTheme.colorScheme.error ) } - PageContent(videoList, videoSelected = { - navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it) - }) { + PageContent( + videoList, + codecsCollection, + updateFilter = { audioCodec, videoCodec -> load(audioCodec, videoCodec) }, + videoSelected = { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it) }) { scope.launch { loading = true viewModel.refresh() @@ -136,24 +137,23 @@ fun HomeScreen() { @Composable -fun PageContent(list: List, videoSelected: (VideoFile) -> Unit, onRefresh: () -> Unit) { +fun PageContent( + list: Map, + codecs: CodecsCollection?, + updateFilter: (String?, String?) -> Unit, + videoSelected: (String) -> Unit, + onRefresh: () -> Unit +) { var filteredAudioCodec by remember { mutableStateOf(null) } var filteredVideoCodec by remember { mutableStateOf(null) } var filteredName by remember { mutableStateOf("") } - val filteredVideos = list.filter { - it.fileName.contains( - filteredName, - ignoreCase = true - ) && it.videos.any { v -> - filteredVideoCodec?.let { fv -> v.codec == fv } ?: true - } && it.audios.any { a -> filteredAudioCodec?.let { fa -> a.codec == fa } ?: true } - } + val filteredVideos = list.filter { it.value.contains(filteredName, ignoreCase = true) }.map { it.key to it.value } Column { val filterMod = Modifier.padding(8.dp, 0.dp).fillMaxWidth() Column(modifier = filterMod, horizontalAlignment = Alignment.CenterHorizontally) { - val videoOptions = list.asSequence().flatMap { it.videos }.map { it.codec }.toSet().toList().sorted() - val audioOptions = list.asSequence().flatMap { it.audios }.map { it.codec }.toSet().toList().sorted() + val videoOptions = codecs?.video ?: emptyList() + val audioOptions = codecs?.audio ?: emptyList() @OptIn(ExperimentalComposeUiApi::class) if (getClientConfig().isDebugEnabled()) { @@ -171,8 +171,14 @@ fun PageContent(list: List, videoSelected: (VideoFile) -> Unit, onRef onValueChange = { filteredName = it }, label = { Text("File name") }, leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = "Search") }) - ComboBox("Video Codecs", videoOptions, filteredVideoCodec, modifier = filterMod) { filteredVideoCodec = it } - ComboBox("Audio Codecs", audioOptions, filteredAudioCodec, modifier = filterMod) { filteredAudioCodec = it } + ComboBox("Video Codecs", videoOptions, filteredVideoCodec, modifier = filterMod) { + filteredVideoCodec = it + updateFilter(filteredAudioCodec, filteredVideoCodec) + } + ComboBox("Audio Codecs", audioOptions, filteredAudioCodec, modifier = filterMod) { + filteredAudioCodec = it + updateFilter(filteredAudioCodec, filteredVideoCodec) + } Row(horizontalArrangement = Arrangement.SpaceEvenly) { IconButton(modifier = sidePad, onClick = onRefresh) { Icon(Icons.Rounded.Refresh, contentDescription = "Refresh") @@ -183,6 +189,7 @@ fun PageContent(list: List, videoSelected: (VideoFile) -> Unit, onRef filteredName = "" filteredAudioCodec = null filteredVideoCodec = null + updateFilter(filteredAudioCodec, filteredVideoCodec) }) { Icon(Icons.Rounded.Close, contentDescription = "Clear filters") } @@ -192,7 +199,7 @@ fun PageContent(list: List, videoSelected: (VideoFile) -> Unit, onRef Row(modifier = Modifier.fillMaxSize().padding(8.dp)) { LazyColumn { items(filteredVideos) { file -> - VideoRow(file) { videoSelected(it) } + VideoRow(file.second) { videoSelected(file.first) } } } } @@ -200,7 +207,7 @@ fun PageContent(list: List, videoSelected: (VideoFile) -> Unit, onRef } @Composable -private fun VideoRow(file: VideoFile, onClick: (VideoFile) -> Unit) { +private fun VideoRow(file: String, onClick: (String) -> Unit) { Card( onClick = { onClick(file) }, modifier = Modifier.fillMaxWidth().padding(4.dp).hoverable(MutableInteractionSource()), @@ -208,15 +215,10 @@ private fun VideoRow(file: VideoFile, onClick: (VideoFile) -> Unit) { ) { Column(modifier = Modifier.fillMaxWidth()) { Text( - text = file.fileName, + text = file, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.fillMaxWidth().padding(8.dp) ) - HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) - Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly) { - Text(text = file.videoInfo, style = MaterialTheme.typography.bodyMedium) - Text(text = file.audioInfo, style = MaterialTheme.typography.bodyMedium) - } } } } diff --git a/composeApp/src/commonMain/kotlin/viewmodels/AppViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodels/AppViewModel.kt new file mode 100644 index 0000000..02385d7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/viewmodels/AppViewModel.kt @@ -0,0 +1,28 @@ +package viewmodels + +import io.github.jsixface.common.Api +import io.github.jsixface.common.CodecsCollection +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.resources.get +import io.ktor.http.isSuccess +import kotlinx.coroutines.flow.flow +import ui.model.ModelState +import ui.model.ModelState.Error +import ui.model.ModelState.Init +import ui.model.ModelState.Success + +class AppViewModel(private val client: HttpClient) { + + val codecsCollection = flow> { + emit(Init()) + runCatching { client.get(Api.Codecs) }.onSuccess { resp -> + when { + resp.status.isSuccess() -> emit(Success(resp.body())) + else -> emit(Error("Error. Status: ${resp.status}")) + } + }.onFailure { + emit(Error("Error. Status: ${it.message}")) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/viewmodels/VideoListViewModel.kt b/composeApp/src/commonMain/kotlin/viewmodels/VideoListViewModel.kt index 05b81db..ba6943c 100644 --- a/composeApp/src/commonMain/kotlin/viewmodels/VideoListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/viewmodels/VideoListViewModel.kt @@ -25,9 +25,11 @@ class VideoListViewModel(private val client: HttpClient) { log("New VideoListViewModel") } - val videoList = flow>> { + fun videoList(audioFilter: String? = null, videoFilter: String? = null) = flow>> { emit(Init()) - runCatching { client.get(Api.Videos) }.onSuccess { resp -> + runCatching { + client.get(Api.Videos(videoFilter = videoFilter, audioFilter = audioFilter)) + }.onSuccess { resp -> when { resp.status.isSuccess() -> emit(Success(resp.body())) else -> emit(Error("Error. Status: ${resp.status}")) @@ -37,14 +39,28 @@ class VideoListViewModel(private val client: HttpClient) { } } - suspend fun submitJob(videoFile: VideoFile, conversions: Map) { - client.post(Api.Videos.Video(path = videoFile.fileName)) { + fun getVideoFile(path: String) = flow> { + emit(Init()) + runCatching { + client.get(Api.Videos.Video(path = path)) + }.onSuccess { resp -> + when { + resp.status.isSuccess() -> emit(Success(resp.body())) + else -> emit(Error("Error. Status: ${resp.status}")) + } + }.onFailure { + emit(Error("Error. Status: ${it.message}")) + } + } + + suspend fun submitJob(videoFile: String, conversions: Map) { + client.post(Api.Videos.Video(path = videoFile)) { setBody(conversions) contentType(ContentType.Application.Cbor) } } suspend fun refresh() { - client.patch(Api.Videos).status + client.patch(Api.Videos()).status } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/Previews.kt b/composeApp/src/desktopMain/kotlin/Previews.kt index 5b89574..aec09bf 100644 --- a/composeApp/src/desktopMain/kotlin/Previews.kt +++ b/composeApp/src/desktopMain/kotlin/Previews.kt @@ -10,8 +10,6 @@ import ui.BackendDialogContent import ui.BackupContent import ui.JobContent import ui.ListEditor -import ui.home.FileDetails -import ui.home.PageContent import ui.theme.AppTheme import kotlin.random.Random @@ -89,19 +87,3 @@ fun previewBackendDialog() { BackendDialogContent("helium.home", {}, {}) } } - -@Preview -@Composable -private fun seeFileDetails() { - AppTheme { - FileDetails(videos[0]) {} - } -} - -@Preview -@Composable -private fun seeVideoScreen() { - AppTheme { - PageContent(videos, {}) {} - } -} \ No newline at end of file diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/api/VideoApi.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/api/VideoApi.kt index 9939526..e318114 100644 --- a/server/src/main/kotlin/io/github/jsixface/codexvert/api/VideoApi.kt +++ b/server/src/main/kotlin/io/github/jsixface/codexvert/api/VideoApi.kt @@ -4,24 +4,47 @@ import io.github.jsixface.codexvert.db.IVideoFilesRepo import io.github.jsixface.codexvert.ffprobe.IParser import io.github.jsixface.codexvert.logger import io.github.jsixface.codexvert.utils.toVideoFile +import io.github.jsixface.common.CodecsCollection import io.github.jsixface.common.VideoFile import java.nio.file.Path -typealias VideoList = List +typealias VideoList = Map class VideoApi(private val parser: IParser, private val repo: IVideoFilesRepo) { private val logger = logger() + private var cache = emptyList() + + private suspend fun updateCache() { + logger.info("${this::class.simpleName} updateCache()") + cache = repo.getAll().map { it.toVideoFile() } + } suspend fun refreshDirs(): Boolean { logger.info("Refreshing videos...") val data = SavedData.load() - return parser.parseAll(data.settings.libraryLocations, data.settings.videoExtensions) + val refreshed = parser.parseAll(data.settings.libraryLocations, data.settings.videoExtensions) + if (refreshed) updateCache() + return refreshed } - suspend fun getVideos(): VideoList = repo.getAll().map { it.toVideoFile() } + fun getVideos(audioFilter: String?, videoFilter: String?): VideoList { + return cache.filter { audioFilter?.let { af -> it.audios.any { a -> a.codec == af } } ?: true } + .filter { videoFilter?.let { vf -> it.videos.any { v -> v.codec == vf } } ?: true } + .associate { it.path to it.fileName } + } suspend fun getVideo(path: String): VideoFile? { return repo.getFile(Path.of(path))?.toVideoFile() } + + suspend fun getCodecsPresent(): CodecsCollection { + if (cache.isEmpty()) updateCache() + + val allVids = cache + val codecV = allVids.flatMap { it.videos }.map { it.codec }.distinct().sorted() + val codecA = allVids.flatMap { it.audios }.map { it.codec }.distinct().sorted() + val codecS = allVids.flatMap { it.subtitles }.map { it.codec }.distinct().sorted() + return CodecsCollection(video = codecV, audio = codecA, subtitle = codecS) + } } \ No newline at end of file diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/route/VideoRoutes.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/route/VideoRoutes.kt index a3e1655..43a553d 100644 --- a/server/src/main/kotlin/io/github/jsixface/codexvert/route/VideoRoutes.kt +++ b/server/src/main/kotlin/io/github/jsixface/codexvert/route/VideoRoutes.kt @@ -24,13 +24,13 @@ fun Route.videoRoutes() { val videoApi by inject() val conversionApi by inject() - get { - call.respond(videoApi.getVideos().sortedBy { it.fileName }) + get { v -> + call.respond(videoApi.getVideos(v.audioFilter, v.videoFilter)) } patch { videoApi.refreshDirs() - call.respond(videoApi.getVideos().sortedBy { it.fileName }) + call.respond("OK") } get { video -> @@ -61,4 +61,8 @@ fun Route.videoRoutes() { conversionApi.startConversion(videoFile, data) call.respond("OK") } + + get { + call.respond(videoApi.getCodecsPresent()) + } } diff --git a/shared/src/commonMain/kotlin/io/github/jsixface/common/Api.kt b/shared/src/commonMain/kotlin/io/github/jsixface/common/Api.kt index 9ef2437..83b1abc 100644 --- a/shared/src/commonMain/kotlin/io/github/jsixface/common/Api.kt +++ b/shared/src/commonMain/kotlin/io/github/jsixface/common/Api.kt @@ -5,13 +5,16 @@ import io.ktor.resources.Resource sealed interface Api { @Resource("/videos") - data object Videos : Api { + data class Videos(val videoFilter: String? = null, val audioFilter: String? = null) : Api { @Resource("video") - data class Video(val parent: Videos = Videos, val path: String?) : Api + data class Video(val parent: Videos = Videos(), val path: String?) : Api } + @Resource("/codecs") + data object Codecs : Api + @Resource("/backups") - data object Backups: Api { + data object Backups : Api { @Resource("/backup") data class Backup(val parent: Backups = Backups, val path: String) : Api } diff --git a/shared/src/commonMain/kotlin/io/github/jsixface/common/MediaModels.kt b/shared/src/commonMain/kotlin/io/github/jsixface/common/MediaModels.kt index e565b1e..99cf744 100644 --- a/shared/src/commonMain/kotlin/io/github/jsixface/common/MediaModels.kt +++ b/shared/src/commonMain/kotlin/io/github/jsixface/common/MediaModels.kt @@ -18,6 +18,13 @@ enum class Codec( MPEG4(TrackType.Video, listOf("mpeg4")), } +@Serializable +data class CodecsCollection( + val video: List, + val audio: List, + val subtitle: List, +) + @Serializable sealed class Conversion { @Serializable