Skip to content

Commit

Permalink
Media info in detail view shows all.
Browse files Browse the repository at this point in the history
-- Auto conversion not working
  • Loading branch information
jsixface committed Feb 11, 2025
1 parent ceebacc commit 0cf5284
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 89 deletions.
5 changes: 0 additions & 5 deletions .run/Run Desktop.run.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Desktop" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="DEBUG_MODE" value="1"/>
</map>
</option>
<option name="executionName"/>
<option name="externalProjectPath" value="$PROJECT_DIR$"/>
<option name="externalSystemIdString" value="GRADLE"/>
Expand Down
137 changes: 73 additions & 64 deletions composeApp/src/commonMain/kotlin/ui/home/FileDetails.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package ui.home

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
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
Expand All @@ -28,10 +31,14 @@ import androidx.compose.ui.unit.dp
import io.github.jsixface.common.Codec
import io.github.jsixface.common.Conversion
import io.github.jsixface.common.Conversion.Convert
import io.github.jsixface.common.Conversion.Copy
import io.github.jsixface.common.Conversion.Drop
import io.github.jsixface.common.MediaTrack
import io.github.jsixface.common.TrackType
import io.github.jsixface.common.VideoFile
import org.koin.compose.koinInject
import ui.model.ModelState
import util.log
import viewmodels.VideoListViewModel

@Composable
Expand All @@ -41,12 +48,18 @@ fun FileDetails(file: String, onDismiss: (Map<MediaTrack, Conversion>?) -> Unit)
var errorLoading by remember { mutableStateOf<String?>(null) }
val conversion = remember { mutableStateMapOf<MediaTrack, Conversion>() }
LaunchedEffect(file) {
// re-initialize after loading a different file
videoFile = null
errorLoading = null
conversion.clear()

// Load file details
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 }
it.result.audios.forEach { a -> conversion[a] = Copy }
it.result.videos.forEach { v -> conversion[v] = Copy }
}

is ModelState.Error<*> -> {
Expand All @@ -61,77 +74,73 @@ fun FileDetails(file: String, onDismiss: (Map<MediaTrack, Conversion>?) -> Unit)
}

val padder = Modifier.padding(16.dp)
OutlinedCard(
modifier = padder.width(800.dp),
shape = RoundedCornerShape(16.dp),
Column(
modifier = padder.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = padder.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
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.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 }
}
videoFile?.let { file ->
Row {
Text(file.fileName, style = MaterialTheme.typography.displaySmall, modifier = padder)
}
val tracks = file.videos + file.audios + file.subtitles
if (tracks.isNotEmpty()) tracks.forEach { track ->
CodecRow(track, conversion[track] ?: Copy) {
log("Adding track $track with conversion $it")
conversion[track] = it
}
}

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)
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)
}
}
}


@Composable
private fun CodecRow(ai: Int, track: MediaTrack, selected: Conversion, onSelect: (Conversion) -> Unit) {
val codecsAvailable = Codec.entries.filter { it.type == track.type }
Row(modifier = Modifier.padding(16.dp, 4.dp), verticalAlignment = Alignment.CenterVertically) {
Text("Track $ai", modifier = Modifier.weight(1f))
Row(modifier = Modifier.weight(2f), verticalAlignment = Alignment.CenterVertically) {
Text("Codec: ${track.codec}")
}
Row(modifier = Modifier.weight(5f), verticalAlignment = Alignment.CenterVertically) {
Text("To:")
FilterChip(
onClick = { onSelect(Conversion.Copy) },
selected = (selected == Conversion.Copy),
label = { Text("KEEP") },
modifier = Modifier.padding(3.dp, 0.dp),
)
codecsAvailable.filter { it.name.lowercase() != track.codec.lowercase() }.forEach { c ->
val convert = Convert(c)
private fun CodecRow(track: MediaTrack, selected: Conversion, onSelect: (Conversion) -> Unit) {
val codecsAvailable = getAvailableConversion(track).filterNot { it.name.lowercase() == track.codec.lowercase() }
val actions = buildMap {
put("AS IS", Copy)
put("DELETE", Drop)
codecsAvailable.forEach { put(it.name, Convert(it)) }
}
Row(
modifier = Modifier.padding(16.dp, 4.dp)
.border(border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), shape = RoundedCornerShape(16.dp))
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceAround,
) {
Text(track.typeString())
Icon(Icons.AutoMirrored.Rounded.ArrowForward, contentDescription = "TO")

Row(verticalAlignment = Alignment.CenterVertically) {
actions.forEach { (name, conv) ->
FilterChip(
onClick = { onSelect(convert) },
selected = (selected as? Convert)?.codec == c,
label = { Text(c.name) },
onClick = { onSelect(conv) },
selected = selected == conv,
label = { Text(name) },
modifier = Modifier.padding(3.dp, 0.dp),
)
}
FilterChip(
onClick = { onSelect(Conversion.Drop) },
selected = (selected == Conversion.Drop),
label = { Text("DROP") },
modifier = Modifier.padding(3.dp, 0.dp),
)
}
}
}

private fun MediaTrack.typeString() = when (this) {
is MediaTrack.AudioTrack -> "Audio: $codec ${language.uppercase()}"
is MediaTrack.SubtitleTrack -> "Subtitle: $codec ${language.uppercase()}"
is MediaTrack.VideoTrack -> "Video: $codec"
}

private fun getAvailableConversion(track: MediaTrack): List<Codec> = when (track) {
is MediaTrack.VideoTrack -> Codec.entries.filter { it.type == TrackType.Video }
is MediaTrack.AudioTrack -> Codec.entries.filter { it.type == TrackType.Audio }
is MediaTrack.SubtitleTrack -> Codec.entries.filter { it.type == TrackType.Subtitle }
}
18 changes: 8 additions & 10 deletions composeApp/src/desktopMain/kotlin/Previews.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import androidx.compose.runtime.Composable
import io.github.jsixface.common.AutoConversion
import io.github.jsixface.common.ConversionJob
import io.github.jsixface.common.JobStatus
import io.github.jsixface.common.MediaTrack
import io.github.jsixface.common.TrackType
import io.github.jsixface.common.VideoFile
import ui.BackupContent
import ui.JobContent
Expand All @@ -22,25 +20,25 @@ private val videos = listOf(
"Friends.S05E01.DVDrip.XviD-SAiNTS.avi",
234,
audios = listOf(
MediaTrack(TrackType.Audio, 0, "mp3"),
MediaTrack(TrackType.Audio, 1, "aac")
// MediaTrack(TrackType.Audio, 0, "mp3"),
// MediaTrack(TrackType.Audio, 1, "aac")
),
videos = listOf(
MediaTrack(TrackType.Video, 0, "hevc"),
MediaTrack(TrackType.Video, 1, "mp4")
// MediaTrack(TrackType.Video, 0, "hevc"),
// MediaTrack(TrackType.Video, 1, "mp4")
)
),
VideoFile(
"Friends.S05E02.DVDrip.XviD-SAiNTS.avi",
"Friends.S05E02.DVDrip.XviD-SAiNTS.avi",
234,
audios = listOf(
MediaTrack(TrackType.Audio, 0, "mp3"),
MediaTrack(TrackType.Audio, 1, "aac")
// MediaTrack(TrackType.Audio, 0, "mp3"),
// MediaTrack(TrackType.Audio, 1, "aac")
),
videos = listOf(
MediaTrack(TrackType.Video, 0, "hevc"),
MediaTrack(TrackType.Video, 1, "mp4")
// MediaTrack(TrackType.Video, 0, "hevc"),
// MediaTrack(TrackType.Video, 1, "mp4")
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ class ConversionApi {

private fun conversionParams(convSpecs: Map<MediaTrack, Conversion>, file: VideoFile): List<String> {
val result = mutableListOf<String>()
val missingTracks = (file.subtitles + file.videos + file.audios).filter { it !in convSpecs.keys }
val convIndices = convSpecs.keys.map { it.index }
val missingTracks = (file.subtitles + file.videos + file.audios).filter { it.index !in convIndices }
val copyTracks = missingTracks.map { it to Conversion.Copy }
val allConversion = convSpecs + copyTracks
allConversion.toList().forEachIndexed { i, (track, conv) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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

Expand Down Expand Up @@ -56,8 +55,11 @@ fun VideoFileEntity.toVideoFile(): VideoFile = transaction {
)
}

private fun VideoEntity.toMediaTrack() = MediaTrack(TrackType.Video, index, codec)
private fun VideoEntity.toMediaTrack() = MediaTrack.VideoTrack(
codec, index, codecTag, profile, resolution, aspectRatio, frameRate, bitRate, bitDepth, pixelFormat
)

private fun AudioEntity.toMediaTrack() = MediaTrack(TrackType.Audio, index, codec)
private fun AudioEntity.toMediaTrack() =
MediaTrack.AudioTrack(codec, index, channels, layout, bitrate, sampleRate, language)

private fun SubtitleEntity.toMediaTrack() = MediaTrack(TrackType.Subtitle, index, codec)
private fun SubtitleEntity.toMediaTrack() = MediaTrack.SubtitleTrack(codec, index, language)
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,49 @@ enum class TrackType {
}

@Serializable
data class MediaTrack(
val type: TrackType,
val index: Int,
val codec: String
)
sealed class MediaTrack {
abstract val codec: String
abstract val index: Int

override fun toString(): String {
return "[$index:${codec.uppercase()}]"
}

@Serializable
@SerialName("video")
class VideoTrack(
override val codec: String,
override val index: Int,
val codecTag: String,
val profile: String,
val resolution: String,
val aspectRatio: String,
val frameRate: Float,
val bitRate: Int,
val bitDepth: Int,
val pixelFormat: String,
) : MediaTrack()

@Serializable
@SerialName("audio")
class AudioTrack(
override val codec: String,
override val index: Int,
val channels: Int,
val layout: String,
val bitRate: Int,
val sampleRate: String,
val language: String,
) : MediaTrack()

@Serializable
@SerialName("subtitle")
class SubtitleTrack(
override val codec: String,
override val index: Int,
val language: String
) : MediaTrack()
}

fun MediaTrack.isDolby() = codec.lowercase() in listOf("ac3", "eac3")

Expand Down

0 comments on commit 0cf5284

Please sign in to comment.