Skip to content

Commit

Permalink
Added logic for db access and storage
Browse files Browse the repository at this point in the history
  • Loading branch information
jsixface committed Jan 18, 2025
1 parent 60b4d1d commit f1ef78e
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 14 deletions.
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ database = [
"exposed-jdbc",
"exposed-json",
"liquibase-core",
"sqlite"
]
koin-server = [
"koin-core",
Expand Down
2 changes: 2 additions & 0 deletions server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ dependencies {
implementation(libs.kotlinx.json)
implementation(libs.bundles.koin.server)

runtimeOnly(libs.sqlite)

// testImplementation(libs.ktor.server.tests)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.bundles.koin.test)
Expand Down
15 changes: 9 additions & 6 deletions server/src/main/kotlin/io/github/jsixface/codexvert/db/Dto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ class VideoEntity(id: EntityID<Int>) : IntEntity(id) {
var bitRate by VideosTable.bitRate
var bitDepth by VideosTable.bitDepth
var pixelFormat by VideosTable.pixelFormat
val videoFile by VideoFileEntity referrersOn VideosTable.videoFile
var videoFile by VideoFileEntity referencedOn VideosTable.videoFile
}

object AudiosTable : IntIdTable() {
val index = integer("index")
val codec = varchar("codec", 50)
val channels = varchar("channels", 50)
val channels = integer("channels")
val layout = varchar("layout", 50)
val bitrate = integer("bit_rate")
val sampleRate = integer("sample_rate")
val sampleRate = varchar("sample_rate", 25)
val language = varchar("language", 50)
val videoFile = reference("video_file_id", VideoFilesTable, onDelete = ReferenceOption.CASCADE)
}
Expand All @@ -58,7 +58,7 @@ class AudioEntity(id: EntityID<Int>) : IntEntity(id) {
var bitrate by AudiosTable.bitrate
var sampleRate by AudiosTable.sampleRate
var language by AudiosTable.language
val videoFile by VideoFileEntity referrersOn AudiosTable.videoFile
var videoFile by VideoFileEntity referencedOn AudiosTable.videoFile
}

object SubtitlesTable : IntIdTable() {
Expand All @@ -74,7 +74,7 @@ class SubtitleEntity(id: EntityID<Int>) : IntEntity(id) {
var index by SubtitlesTable.index
var codec by SubtitlesTable.codec
var language by SubtitlesTable.language
val videoFile by VideoFileEntity referrersOn SubtitlesTable.videoFile
var videoFile by VideoFileEntity referencedOn SubtitlesTable.videoFile
}

object VideoFilesTable : IntIdTable() {
Expand All @@ -90,7 +90,10 @@ class VideoFileEntity(id: EntityID<Int>) : IntEntity(id) {

var path by VideoFilesTable.path
var name by VideoFilesTable.name
val sizeMb by VideoFilesTable.sizeMb
var sizeMb by VideoFilesTable.sizeMb
val videoStream by VideoEntity backReferencedOn VideosTable.videoFile
val audioStreams by AudioEntity referrersOn AudiosTable.videoFile
val subtitles by SubtitleEntity referrersOn SubtitlesTable.videoFile
var modified by VideoFilesTable.modified
var added by VideoFilesTable.added
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.github.jsixface.codexvert.db

import io.github.jsixface.codexvert.ffprobe.ProbeInfo
import io.github.jsixface.codexvert.ffprobe.ProbeStream
import io.github.jsixface.codexvert.logger
import io.github.jsixface.codexvert.utils.updateInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.fileSize
import kotlin.io.path.getLastModifiedTime


interface IVideoFilesRepo {
suspend fun getAll(): List<VideoFileEntity>
suspend fun get(id: Int): VideoFileEntity?
suspend fun getFile(path: Path): VideoFileEntity?
suspend fun delete(id: Int)
suspend fun update(videoInfo: ProbeInfo, entity: VideoFileEntity): Boolean
suspend fun create(videoInfo: ProbeInfo, file: Path): VideoFileEntity
}

class VideoFilesRepo(private val db: Database) : IVideoFilesRepo {
private val logger = logger()

private suspend fun <T> dbQuery(block: suspend () -> T): T =
newSuspendedTransaction(Dispatchers.IO) { block() }

override suspend fun getAll() = dbQuery { VideoFileEntity.all().toList() }

override suspend fun get(id: Int) = dbQuery { VideoFileEntity.findById(id) }

override suspend fun getFile(path: Path) = dbQuery {
VideoFileEntity.find { VideoFilesTable.path eq path.toAbsolutePath().toString() }.firstOrNull()
}

override suspend fun create(videoInfo: ProbeInfo, file: Path): VideoFileEntity {
return dbQuery {
val v = VideoFileEntity.new {
path = file.absolutePathString()
name = file.fileName.toString()
sizeMb = file.fileSize().toInt() / 1024 / 1024
modified = file.getLastModifiedTime().toMillis()
added = Clock.System.now().toEpochMilliseconds()
}
logger.debug("Added video file: ${v.path}")
videoInfo.streams.forEach { createStream(it, v) }
v
}
}

private fun createStream(stream: ProbeStream, v: VideoFileEntity) {
when (stream.codecType) {
"audio" -> {
AudioEntity.new {
videoFile = v
updateInfo(stream)
}
}

"video" -> {
VideoEntity.new {
videoFile = v
updateInfo(stream)
}
}

"subtitle" -> {
SubtitleEntity.new {
videoFile = v
index = stream.index
codec = stream.codecName
language = stream.tags["language"] ?: ""
}
}
}
}


override suspend fun delete(id: Int) {
dbQuery {
val v = get(id)
logger.debug("deleting video file: ${v?.path}")
v?.delete()
}
}

override suspend fun update(videoInfo: ProbeInfo, entity: VideoFileEntity) = dbQuery {
try {
entity.videoStream.delete()
entity.audioStreams.forEach { it.delete() }
entity.subtitles.forEach { it.delete() }
videoInfo.streams.forEach { createStream(it, entity) }
true
} catch (e: Exception) {
false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.jsixface.codexvert.ffprobe

import io.github.jsixface.codexvert.db.IVideoFilesRepo
import io.github.jsixface.codexvert.logger
import java.nio.file.Path
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.walk


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

class Parser(private val repo: IVideoFilesRepo) : IParser {
private val logger = logger()

override suspend fun parseVideoFile(file: Path) {
val p = ProbeUtils.parseMediaInfo(file) ?: return
val entity = repo.getFile(file)
if (entity == null) {
repo.create(p, file)
} else {
repo.update(p, entity)
}
}

override suspend fun parseAll(locations: List<String>, extensions: List<String>) {
val videos = locations.map { Path(it) }.flatMap { loc ->
loc.walk(PathWalkOption.FOLLOW_LINKS)
.filter { it.isDirectory().not() }
.filter { extensions.contains(it.extension.lowercase()) }
}.associateWith { it.getLastModifiedTime().toMillis() }
.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)) }
modified.keys.forEach { parseVideoFile(Path(it)) }
deleted.values.forEach { repo.delete(it.id.value) }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.jsixface.codexvert.ffprobe

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ProbeStream(
val index: Int,
@SerialName("codec_name") val codecName: String = "",
@SerialName("codec_type") val codecType: String,
@SerialName("codec_tag_string") val codecTagName: String = "",
val profile: String = "",
val width: Int = 0,
val height: Int = 0,
@SerialName("display_aspect_ratio") val aspectRatio: String = "",
@SerialName("avg_frame_rate") val frameRate: String = "",
val bitRate: String = "",
val bitDepth: String = "",
@SerialName("pix_fmt") val pixelFormat: String = "",

val channels: Int = 1,
@SerialName("channel_layout") val channelLayout: String = "",
@SerialName("sample_rate") val sampleRate: String = "",
@SerialName("tags") val tags: Map<String, String> = emptyMap(),
)

@Serializable
data class ProbeFormat(
@SerialName("filename") val fileName: String,
@SerialName("nb_streams") val numStreams: Int,
@SerialName("format_long_name") val formatName: String,
@SerialName("duration") val duration: String = "",
)

@Serializable
data class ProbeInfo(
val streams: List<ProbeStream>,
val format: ProbeFormat,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.github.jsixface.codexvert.ffprobe

import io.github.jsixface.codexvert.logger
import kotlinx.serialization.json.Json
import java.nio.file.Path

object ProbeUtils {
private val logger = logger()
private val json = Json {
ignoreUnknownKeys = true
}

fun parseMediaInfo(path: Path): ProbeInfo? {
logger.info("Parsing file $path")
val builder =
ProcessBuilder(
"ffprobe",
"-v", "error",
"-of", "json",
"-pretty",
"-show_entries", "stream:program:format:chapter",
path.toAbsolutePath().toString(),
)
return try {
val process = builder.start()
val 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)
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import io.github.jsixface.codexvert.api.ConversionApi
import io.github.jsixface.codexvert.api.JobsApi
import io.github.jsixface.codexvert.api.SettingsApi
import io.github.jsixface.codexvert.api.VideoApi
import io.github.jsixface.codexvert.db.IVideoFilesRepo
import io.github.jsixface.codexvert.db.VideoFilesRepo
import io.github.jsixface.codexvert.db.getDb
import io.github.jsixface.codexvert.ffprobe.IParser
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.dsl.module
import org.koin.logger.slf4jLogger

fun Application.configureKoin() {
val koinModule = module {
single { getDb() }
single<Database> { getDb() }
single<IVideoFilesRepo> { VideoFilesRepo(get()) }
single<IParser> { Parser(get()) }
single { VideoApi() }
single { BackupApi() }
single { ConversionApi() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package io.github.jsixface.codexvert.plugins

import io.github.jsixface.codexvert.route.backupRoutes
import io.github.jsixface.codexvert.route.jobRoutes
import io.github.jsixface.codexvert.route.videoRoutes
import io.github.jsixface.codexvert.route.backupRoutes
import io.github.jsixface.route.settingsRoutes
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.resources.*
import io.ktor.server.routing.*
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.http.content.staticFiles
import io.ktor.server.resources.Resources
import io.ktor.server.routing.routing
import java.io.File

fun Application.configureRouting() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.github.jsixface.codexvert.utils

class AspectRatio(private val w: Float, private val h: Float) {

override fun toString(): String {
val (wi, hi) = when {
w < 10 || h < 10 -> w to h
w > h -> w / h to 1.0f
else -> 1.0f to h / w
}
return "${wi.shortString()}:${hi.shortString()}"
}

companion object {
operator fun invoke(ratio: String): AspectRatio {
val measures = ratio.split(":").mapNotNull { it.toFloatOrNull() }
assert(measures.size == 2) { "Should be in the format 'w:h" }
return AspectRatio(measures[0], measures[1])
}
}
}
Loading

0 comments on commit f1ef78e

Please sign in to comment.