diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 690eb66..3c6cf5f 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -49,6 +49,7 @@ kotlin {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.ktor.client.cio)
+ implementation(libs.logback)
}
wasmJsMain.dependencies {
implementation(libs.ktor.client.js)
diff --git a/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt
index 856af0a..1d7e7f1 100644
--- a/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt
+++ b/composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt
@@ -100,21 +100,23 @@ fun HomeScreen() {
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
- if (loading) CircularProgressIndicator(modifier = sidePad)
- if (errorLoading) {
- Text(
- "Error loading list",
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.error
- )
- }
- PageContent(videoList, videoSelected = {
- navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it)
- }) {
- scope.launch {
- loading = true
- viewModel.refresh()
- load()
+ Column {
+ if (loading) CircularProgressIndicator(modifier = sidePad)
+ if (errorLoading) {
+ Text(
+ "Error loading list",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+ PageContent(videoList, videoSelected = {
+ navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it)
+ }) {
+ scope.launch {
+ loading = true
+ viewModel.refresh()
+ load()
+ }
}
}
}
diff --git a/composeApp/src/desktopMain/resources/logback.xml b/composeApp/src/desktopMain/resources/logback.xml
new file mode 100644
index 0000000..2f06544
--- /dev/null
+++ b/composeApp/src/desktopMain/resources/logback.xml
@@ -0,0 +1,12 @@
+
+
+
+ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
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 2614eaf..9939526 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
@@ -1,73 +1,27 @@
package io.github.jsixface.codexvert.api
+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.CodecUtils.parseMediaInfo
-import io.github.jsixface.common.TrackType
+import io.github.jsixface.codexvert.utils.toVideoFile
import io.github.jsixface.common.VideoFile
-import kotlin.io.path.Path
-import kotlin.io.path.PathWalkOption
-import kotlin.io.path.extension
-import kotlin.io.path.getLastModifiedTime
-import kotlin.io.path.isDirectory
-import kotlin.io.path.name
-import kotlin.io.path.pathString
-import kotlin.io.path.walk
+import java.nio.file.Path
-typealias VideoList = Map
+typealias VideoList = List
-
-class VideoApi {
+class VideoApi(private val parser: IParser, private val repo: IVideoFilesRepo) {
private val logger = logger()
- fun refreshDirs(): Boolean {
+ suspend fun refreshDirs(): Boolean {
+ logger.info("Refreshing videos...")
val data = SavedData.load()
- val scan = mutableMapOf()
- data.settings.libraryLocations.forEach { l ->
- val loc = Path(l)
- loc.walk(PathWalkOption.FOLLOW_LINKS).forEach { p ->
- if (p.isDirectory().not() && data.settings.videoExtensions.contains(p.extension.lowercase())) {
- scan[p.pathString] = VideoFile(
- path = p.pathString,
- fileName = p.name,
- modifiedTime = p.getLastModifiedTime().toMillis()
- )
- }
- }
- }
- val toParse: VideoList = consolidateData(data.details, scan)
- val deleted = removeDeleted(data.details, scan)
- parseMediaFiles(data.details, toParse)
- return if (toParse.isNotEmpty() || deleted) {
- logger.info("Saving new data")
- data.save()
- true
- } else false
+ return parser.parseAll(data.settings.libraryLocations, data.settings.videoExtensions)
}
- fun getVideos(): VideoList = SavedData.load().details
-
- private fun parseMediaFiles(details: MutableMap, toParse: VideoList) {
- toParse.values.forEach { videoFile ->
- parseMediaInfo(videoFile.path)?.let { tracks ->
- details[videoFile.path] = videoFile.copy(
- videos = tracks.filter { t -> t.type == TrackType.Video },
- audios = tracks.filter { t -> t.type == TrackType.Audio },
- subtitles = tracks.filter { t -> t.type == TrackType.Subtitle },
- )
- }
- }
- }
-
- private fun consolidateData(data: MutableMap, scan: VideoList): VideoList {
- val toScan = scan.filterValues { data[it.path]?.modifiedTime != it.modifiedTime }
- toScan.forEach { data[it.key] = it.value } // Source object change
- return toScan
- }
+ suspend fun getVideos(): VideoList = repo.getAll().map { it.toVideoFile() }
- private fun removeDeleted(data: MutableMap, scan: VideoList): Boolean {
- val toDelete = data.keys.filter { !scan.containsKey(it) }
- toDelete.forEach { data.remove(it) }
- return toDelete.isNotEmpty()
+ suspend fun getVideo(path: String): VideoFile? {
+ return repo.getFile(Path.of(path))?.toVideoFile()
}
}
\ No newline at end of file
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/db/VideoFilesRepo.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/db/VideoFilesRepo.kt
index f3ce3e8..00976e2 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/db/VideoFilesRepo.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/db/VideoFilesRepo.kt
@@ -84,6 +84,9 @@ class VideoFilesRepo(private val db: Database) : IVideoFilesRepo {
dbQuery {
val v = get(id)
logger.debug("deleting video file: ${v?.path}")
+ v?.videoStream?.delete()
+ v?.audioStreams?.forEach { it.delete() }
+ v?.subtitles?.forEach { it.delete() }
v?.delete()
}
}
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/Parser.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/Parser.kt
index d62eb72..34bb32a 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/Parser.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/Parser.kt
@@ -13,7 +13,7 @@ import kotlin.io.path.walk
interface IParser {
suspend fun parseVideoFile(file: Path)
- suspend fun parseAll(locations: List, extensions: List)
+ suspend fun parseAll(locations: List, extensions: List): Boolean
}
class Parser(private val repo: IVideoFilesRepo) : IParser {
@@ -29,7 +29,7 @@ class Parser(private val repo: IVideoFilesRepo) : IParser {
}
}
- override suspend fun parseAll(locations: List, extensions: List) {
+ override suspend fun parseAll(locations: List, extensions: List): Boolean {
val videos = locations.map { Path(it) }.flatMap { loc ->
loc.walk(PathWalkOption.FOLLOW_LINKS)
.filter { it.isDirectory().not() }
@@ -38,13 +38,12 @@ class Parser(private val repo: IVideoFilesRepo) : IParser {
.mapKeys { it.key.toAbsolutePath().toString() }
val entries = repo.getAll().associateBy { it.path }
- val added = videos - entries.keys
val deleted = entries - videos.keys
- val modified = videos.filter { it.value != entries[it.key]?.modified } - added.keys
- logger.info("Total files: ${videos.size}, Added: ${added.size}, Deleted: ${deleted.size}, Modified: ${modified.size}")
- added.keys.forEach { parseVideoFile(Path(it)) }
+ val modified = videos.filter { it.value != entries[it.key]?.modified }
+ logger.info("Total files: ${videos.size}, Deleted: ${deleted.size}, Added/Modified: ${modified.size}")
modified.keys.forEach { parseVideoFile(Path(it)) }
deleted.values.forEach { repo.delete(it.id.value) }
+ return deleted.isNotEmpty() || modified.isNotEmpty()
}
}
\ No newline at end of file
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/ProbeUtils.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/ProbeUtils.kt
index 6253ea7..bfad022 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/ProbeUtils.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/ffprobe/ProbeUtils.kt
@@ -21,15 +21,16 @@ object ProbeUtils {
"-show_entries", "stream:program:format:chapter",
path.toAbsolutePath().toString(),
)
+ var output = ""
return try {
val process = builder.start()
- val output = process.inputStream.use { it.bufferedReader().readText() }
+ output = process.inputStream.use { it.bufferedReader().readText() }
process.waitFor()
val probeInfo = json.decodeFromString(output)
logger.debug("Probe info: $probeInfo")
probeInfo
} catch (e: Exception) {
- logger.error("Cant get file information for $path", e)
+ logger.error("Cant get file information for $path. Output = $output", e)
null
}
}
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Koin.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Koin.kt
index 692ab2b..3b28b61 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Koin.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Koin.kt
@@ -13,20 +13,23 @@ import io.github.jsixface.codexvert.ffprobe.Parser
import io.ktor.server.application.Application
import org.jetbrains.exposed.sql.Database
import org.koin.core.context.startKoin
+import org.koin.core.module.dsl.createdAtStart
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.logger.slf4jLogger
fun Application.configureKoin() {
val koinModule = module {
single { getDb() }
- single { VideoFilesRepo(get()) }
- single { Parser(get()) }
- single { VideoApi() }
- single { BackupApi() }
- single { ConversionApi() }
- single { JobsApi(conversionApi = get()) }
- single(createdAtStart = true) { Watchers(videoApi = get(), conversionApi = get()) }
- single { SettingsApi(watchers = get()) }
+ singleOf(::VideoFilesRepo) bind IVideoFilesRepo::class
+ singleOf(::Parser) bind IParser::class
+ singleOf(::VideoApi)
+ singleOf(::BackupApi)
+ singleOf(::ConversionApi)
+ singleOf(::JobsApi)
+ singleOf(::SettingsApi)
+ singleOf(::Watchers) { createdAtStart() }
}
startKoin {
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Watchers.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Watchers.kt
index 620c6fd..4012fec 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Watchers.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/plugins/Watchers.kt
@@ -3,7 +3,9 @@ package io.github.jsixface.codexvert.plugins
import io.github.jsixface.codexvert.api.ConversionApi
import io.github.jsixface.codexvert.api.SavedData
import io.github.jsixface.codexvert.api.VideoApi
+import io.github.jsixface.codexvert.db.IVideoFilesRepo
import io.github.jsixface.codexvert.logger
+import io.github.jsixface.codexvert.utils.toVideoFile
import io.github.jsixface.common.Codec
import io.github.jsixface.common.Conversion
import io.github.jsixface.common.isDolby
@@ -16,7 +18,11 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration
-class Watchers(private val videoApi: VideoApi, private val conversionApi: ConversionApi) {
+class Watchers(
+ private val videoApi: VideoApi,
+ private val repo: IVideoFilesRepo,
+ private val conversionApi: ConversionApi
+) {
private val logger = logger()
private var watchingJob: Job? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -48,11 +54,11 @@ class Watchers(private val videoApi: VideoApi, private val conversionApi: Conver
}
}
- private fun processChanges() {
+ private suspend fun processChanges() {
// Convert the files that has EAC3 or AC3 codec to AAC codec.
// Make sure those files are not already in the job queue.
- val files = videoApi.getVideos()
- files.values.forEach { videoFile ->
+ val files = repo.getAll().map { it.toVideoFile() }
+ files.forEach { videoFile ->
val dolbyTracks = videoFile.audios.filter { it.isDolby() }
val job = conversionApi.jobs.find { it.videoFile.fileName == videoFile.fileName }
if (dolbyTracks.isNotEmpty() && job == null) {
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 e0188b0..a3e1655 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
@@ -25,18 +25,18 @@ fun Route.videoRoutes() {
val conversionApi by inject()
get {
- call.respond(videoApi.getVideos().values.toList().sortedBy { it.fileName })
+ call.respond(videoApi.getVideos().sortedBy { it.fileName })
}
patch {
videoApi.refreshDirs()
- call.respond(videoApi.getVideos().values.toList().sortedBy { it.fileName })
+ call.respond(videoApi.getVideos().sortedBy { it.fileName })
}
get { video ->
logger.info("Getting video ${video.path}")
video.path?.let {
- val find = videoApi.getVideos().values.find { v -> v.fileName == it }
+ val find = videoApi.getVideo(it)
logger.info("found = $find")
call.respondNullable(find)
} ?: run {
@@ -45,13 +45,13 @@ fun Route.videoRoutes() {
}
post { video ->
- val fileName = video.path ?: run {
+ val videoFilePath = video.path ?: run {
logger.warn("no path in URL")
call.respond(HttpStatusCode.BadRequest)
return@post
}
- val videoFile = videoApi.getVideos().values.find { it.fileName == fileName } ?: run {
- logger.warn("No videos found by name $fileName")
+ val videoFile = videoApi.getVideo(videoFilePath) ?: run {
+ logger.warn("No videos found by name $videoFilePath")
call.respond(HttpStatusCode.BadRequest)
return@post
}
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/utils/CodecUtils.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/utils/CodecUtils.kt
deleted file mode 100644
index 47f4153..0000000
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/utils/CodecUtils.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.github.jsixface.codexvert.utils
-
-import io.github.jsixface.codexvert.logger
-import io.github.jsixface.common.MediaProbeInfo
-import io.github.jsixface.common.MediaTrack
-import io.github.jsixface.common.TrackType
-import kotlinx.serialization.json.Json
-
-object CodecUtils {
- private val logger = logger()
- private val json = Json {
- ignoreUnknownKeys = true
- }
-
- fun parseMediaInfo(path: String): List? {
- logger.info("Parsing file $path")
- val builder =
- ProcessBuilder("ffprobe", "-v", "error", "-show_entries", "stream", "-pretty", "-of", "json", path)
- val probeInfo = runCatching {
- val process = builder.start()
- val output = process.inputStream.use { it.bufferedReader().readText() }
- process.waitFor()
- json.decodeFromString(output)
- }
- probeInfo.exceptionOrNull()?.let { logger.error("Cant get ") }
- return probeInfo.getOrNull()?.streams?.mapNotNull { s ->
- val trackType = when (s.codecType) {
- "audio" -> TrackType.Audio
- "video" -> TrackType.Video
- "subtitle" -> TrackType.Subtitle
- else -> null
- }
- trackType?.let { MediaTrack(trackType, s.index, s.codecName) }
- }
- }
-}
diff --git a/server/src/main/kotlin/io/github/jsixface/codexvert/utils/TypeConverters.kt b/server/src/main/kotlin/io/github/jsixface/codexvert/utils/TypeConverters.kt
index 809166c..ab6ab7a 100644
--- a/server/src/main/kotlin/io/github/jsixface/codexvert/utils/TypeConverters.kt
+++ b/server/src/main/kotlin/io/github/jsixface/codexvert/utils/TypeConverters.kt
@@ -1,8 +1,14 @@
package io.github.jsixface.codexvert.utils
import io.github.jsixface.codexvert.db.AudioEntity
+import io.github.jsixface.codexvert.db.SubtitleEntity
import io.github.jsixface.codexvert.db.VideoEntity
+import io.github.jsixface.codexvert.db.VideoFileEntity
import io.github.jsixface.codexvert.ffprobe.ProbeStream
+import io.github.jsixface.common.MediaTrack
+import io.github.jsixface.common.TrackType
+import io.github.jsixface.common.VideoFile
+import org.jetbrains.exposed.sql.transactions.transaction
fun VideoEntity.updateInfo(stream: ProbeStream) {
index = stream.index
@@ -36,3 +42,22 @@ fun AudioEntity.updateInfo(stream: ProbeStream) {
fun Float.shortString() = if (this == toInt().toFloat()) this.toInt().toString() else "%.2f".format(this)
+fun VideoFileEntity.toVideoFile(): VideoFile = transaction {
+ val videoTrack = videoStream.toMediaTrack()
+ val audioTracks = audioStreams.map { it.toMediaTrack() }
+ val subtitleTracks = subtitles.map { it.toMediaTrack() }
+ VideoFile(
+ path = path,
+ fileName = name,
+ modifiedTime = modified,
+ audios = audioTracks,
+ videos = listOf(videoTrack),
+ subtitles = subtitleTracks,
+ )
+}
+
+private fun VideoEntity.toMediaTrack() = MediaTrack(TrackType.Video, index, codec)
+
+private fun AudioEntity.toMediaTrack() = MediaTrack(TrackType.Audio, index, codec)
+
+private fun SubtitleEntity.toMediaTrack() = MediaTrack(TrackType.Subtitle, index, codec)