Skip to content

Commit

Permalink
Serve data from the database
Browse files Browse the repository at this point in the history
  • Loading branch information
jsixface committed Jan 20, 2025
1 parent f1ef78e commit de6f414
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 135 deletions.
1 change: 1 addition & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 17 additions & 15 deletions composeApp/src/commonMain/kotlin/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions composeApp/src/desktopMain/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
70 changes: 12 additions & 58 deletions server/src/main/kotlin/io/github/jsixface/codexvert/api/VideoApi.kt
Original file line number Diff line number Diff line change
@@ -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<String, VideoFile>
typealias VideoList = List<VideoFile>


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<String, VideoFile>()
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<String, VideoFile>, 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<String, VideoFile>, 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<String, VideoFile>, 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlin.io.path.walk

interface IParser {
suspend fun parseVideoFile(file: Path)
suspend fun parseAll(locations: List<String>, extensions: List<String>)
suspend fun parseAll(locations: List<String>, extensions: List<String>): Boolean
}

class Parser(private val repo: IVideoFilesRepo) : IParser {
Expand All @@ -29,7 +29,7 @@ class Parser(private val repo: IVideoFilesRepo) : IParser {
}
}

override suspend fun parseAll(locations: List<String>, extensions: List<String>) {
override suspend fun parseAll(locations: List<String>, extensions: List<String>): Boolean {
val videos = locations.map { Path(it) }.flatMap { loc ->
loc.walk(PathWalkOption.FOLLOW_LINKS)
.filter { it.isDirectory().not() }
Expand All @@ -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()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProbeInfo>(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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Database> { getDb() }
single<IVideoFilesRepo> { VideoFilesRepo(get()) }
single<IParser> { 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ fun Route.videoRoutes() {
val conversionApi by inject<ConversionApi>()

get<Api.Videos> {
call.respond(videoApi.getVideos().values.toList().sortedBy { it.fileName })
call.respond(videoApi.getVideos().sortedBy { it.fileName })
}

patch<Api.Videos> {
videoApi.refreshDirs()
call.respond(videoApi.getVideos().values.toList().sortedBy { it.fileName })
call.respond(videoApi.getVideos().sortedBy { it.fileName })
}

get<Api.Videos.Video> { 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 {
Expand All @@ -45,13 +45,13 @@ fun Route.videoRoutes() {
}

post<Api.Videos.Video> { 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
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)

0 comments on commit de6f414

Please sign in to comment.