Skip to content

Commit

Permalink
fix image placeholder
Browse files Browse the repository at this point in the history
  • Loading branch information
storytellerF committed Feb 1, 2025
1 parent 08a81c4 commit 4ffdb97
Show file tree
Hide file tree
Showing 27 changed files with 696 additions and 285 deletions.
1 change: 1 addition & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
implementation(libs.lucene.queryparser)
implementation(libs.lucene.analysis.common)
implementation(libs.tika.core)
implementation(libs.kim)

implementation(libs.lucene.backward.codecs)
testImplementation(libs.elasticsearch)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.storyteller_f.media

import com.ashampoo.kim.common.convertToPhotoMetadata
import com.ashampoo.kim.jvm.KimJvm
import com.storyteller_f.shared.model.Dimension
import com.storyteller_f.shared.model.MediaInfo
import com.storyteller_f.shared.model.MediaItem
Expand Down Expand Up @@ -63,7 +65,7 @@ class FileSystemMediaService(private val url: String, base: String) : MediaServi
val file = File(root, "$bucketName/$it")
if (file.exists()) {
val item = stat(it, file)
val dimension = getDimension(item, file)
val dimension = getDimension(file)
MediaInfo("${url}amedia/$it", item, dimension)
} else {
null
Expand All @@ -74,31 +76,22 @@ class FileSystemMediaService(private val url: String, base: String) : MediaServi
}

private fun getDimension(
item: MediaItem,
file: File
) = ImageIO.getImageReadersByMIMEType(item.contentType).asSequence().firstNotNullOfOrNull { reader ->
try {
reader.input = FileImageInputStream(file)
reader.read(reader.minIndex)
Dimension(
reader.getWidth(reader.minIndex),
reader.getHeight(reader.minIndex)
)
} catch (e: Throwable) {
Napier.e(throwable = e) {
"get image dimension failed ${item.name}"
}
) = KimJvm.readMetadata(file)?.convertToPhotoMetadata()?.let {
val width = it.widthPx
val height = it.heightPx
if (width != null && height != null) {
Dimension(width, height)
} else {
null
} finally {
reader.dispose()
}
}

private fun stat(it: String, file: File): MediaItem {
val contentType = kotlin.runCatching {
tika.detect(file)
}.getOrNull() ?: URLConnection.guessContentTypeFromName(file.path)
?: org.apache.http.entity.ContentType.APPLICATION_OCTET_STREAM.mimeType
?: org.apache.http.entity.ContentType.APPLICATION_OCTET_STREAM.mimeType
return MediaItem(
it,
contentType,
Expand Down
54 changes: 53 additions & 1 deletion backend/src/main/kotlin/com/storyteller_f/media/MediaService.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package com.storyteller_f.media

import com.ashampoo.kim.common.convertToPhotoMetadata
import com.ashampoo.kim.jvm.KimJvm
import com.storyteller_f.Backend
import com.storyteller_f.shared.model.AMEDIA_BUCKET
import com.storyteller_f.shared.model.MediaInfo
import com.storyteller_f.shared.type.PrimaryKey
import org.apache.tika.Tika
import java.io.File

data class UploadPack(val name: String, val path: File, val contentType: String? = null)
data class UploadPack(val name: String, val path: File, val contentType: String? = null, val meta: Map<String, String> = emptyMap())

interface MediaService {
fun upload(bucketName: String, list: List<UploadPack>): Result<List<MediaInfo?>>
Expand All @@ -14,3 +20,49 @@ interface MediaService {

fun list(bucketName: String, prefix: String): Result<List<MediaInfo>>
}


fun uploadOneFil(
file: File,
tika: Tika,
backend: Backend,
fileName: String,
lng: PrimaryKey,
contentType: String?
): Result<MediaInfo?> {
val type = checkContentType(file, tika, contentType)
val meta = mutableMapOf<String, String>()
if (type.second?.startsWith("image") == true) {
KimJvm.readMetadata(file)?.convertToPhotoMetadata()?.let {
val width = it.widthPx
val height = it.heightPx
if (width != null && height != null) {
meta.putAll(arrayOf("width" to width.toString(), "height" to height.toString()))
}
}
}
return backend.mediaService.upload(
AMEDIA_BUCKET,
listOf(UploadPack("$lng/$fileName", file, type.first, meta))
).map {
it.firstOrNull()
}
}

private fun checkContentType(
file: File,
tika: Tika,
contentType: String?
): Pair<String?, String?> {
val s = "audio/mp4"
val mimeType = tika.detect(file)
return if (contentType == s) {
if (mimeType == s) {
s
} else {
null
}
} else {
null
} to mimeType
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.storyteller_f.media

import com.storyteller_f.MinIoConnection
import com.storyteller_f.shared.model.Dimension
import com.storyteller_f.shared.model.MediaInfo
import com.storyteller_f.shared.model.MediaItem
import io.github.aakira.napier.Napier
Expand Down Expand Up @@ -36,16 +37,28 @@ class MinIoMediaService(private val connection: MinIoConnection) : MediaService
private fun MinioClient.stat(
bucketName: String,
objName: String
): MediaItem {
): Pair<MediaItem, Dimension?> {
val statObject =
statObject(StatObjectArgs.builder().bucket(bucketName).`object`(objName).build())
val dimension = if (statObject.contentType().startsWith("image")) {
val metadata = statObject.userMetadata()
val width = metadata["width"]?.toIntOrNull()
val height = metadata["height"]?.toIntOrNull()
if (width != null && height != null) {
Dimension(width, height)
} else {
null
}
} else {
null
}
return MediaItem(
objName,
statObject.contentType(),
statObject.size(),
objName.substringAfter("/"),
statObject.lastModified().toLocalDateTime().toKotlinLocalDateTime()
)
) to dimension
}

override fun get(bucketName: String, objList: List<String?>): Result<List<MediaInfo?>> {
Expand All @@ -57,8 +70,8 @@ class MinIoMediaService(private val connection: MinIoConnection) : MediaService
try {
val url = getMinioObjectUrl(bucketName, it)
if (url != null) {
val item = stat(bucketName, it)
MediaInfo(url, item, null)
val (item, dimension) = stat(bucketName, it)
MediaInfo(url, item, dimension)
} else {
null
}
Expand All @@ -79,12 +92,13 @@ class MinIoMediaService(private val connection: MinIoConnection) : MediaService
if (!bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
makeBucket(MakeBucketArgs.builder().bucket(bucketName).build())
}
val names = list.map { (objName, picFullPath, type) ->
val names = list.map { (objName, picFullPath, type, meta) ->
val response = uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.`object`(objName)
.filename(picFullPath.absolutePath)
.userMetadata(meta)
.apply {
if (type != null) {
contentType(type)
Expand Down
1 change: 1 addition & 0 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation(libs.jackson.module.kotlin)
implementation(libs.cryptography.provider.jdk)
implementation(libs.napier)
implementation(libs.tika.core)
}

tasks.test {
Expand Down
27 changes: 14 additions & 13 deletions cli/src/main/kotlin/com/storyteller_f/cli/AddPreset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import com.storyteller_f.DatabaseFactory
import com.storyteller_f.ROOM_ID_LENGTH
import com.storyteller_f.crypto_jvm.addProviderForJvm
import com.storyteller_f.index.TopicDocument
import com.storyteller_f.media.UploadPack
import com.storyteller_f.media.uploadOneFil
import com.storyteller_f.shared.*
import com.storyteller_f.shared.model.AMEDIA_BUCKET
import com.storyteller_f.shared.obj.PresetCommunity
import com.storyteller_f.shared.obj.PresetTopic
import com.storyteller_f.shared.obj.PresetValue
Expand All @@ -22,6 +21,7 @@ import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import kotlinx.coroutines.runBlocking
import org.apache.tika.Tika
import org.jetbrains.exposed.sql.JoinType
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
Expand Down Expand Up @@ -50,13 +50,14 @@ class AddPreset : Subcommand("add", "add entry") {
ObjectMapper().registerModule(KotlinModule.Builder().build())
.readValue<PresetValue>(jsonFile.readText())
val parentDir = jsonFile.parentFile.canonicalFile
val tika = Tika()
runBlocking {
try {
when (val type = presetValue.type) {
"community" -> addCommunity(presetValue, parentDir)
"user" -> addUsers(presetValue, parentDir)
"topic" -> addTopics(presetValue, parentDir)
"room" -> addRooms(presetValue, parentDir)
"community" -> addCommunity(presetValue, parentDir, tika)
"user" -> addUsers(presetValue, parentDir, tika)
"topic" -> addTopics(presetValue, parentDir, tika)
"room" -> addRooms(presetValue, parentDir, tika)
else -> {
println("unrecognized type $type")
exitProcess(2)
Expand All @@ -73,7 +74,7 @@ class AddPreset : Subcommand("add", "add entry") {
}
}

private suspend fun addRooms(presetValue: PresetValue, parentDir: File?) {
private suspend fun addRooms(presetValue: PresetValue, parentDir: File?, tika: Tika) {
val l = presetValue.roomData ?: return
Napier.i {
"rooms count ${presetValue.roomData?.size}"
Expand All @@ -86,7 +87,7 @@ class AddPreset : Subcommand("add", "add entry") {
} else {
val path = File(parentDir, icon)
val p = "$id/room-icon.${path.extension}"
backend.mediaService.upload(AMEDIA_BUCKET, listOf(UploadPack(p, path)))
uploadOneFil(path, tika, backend, "room-icon.${path.extension}", id, null).getOrThrow()
Triple(it, p, id)
}
}
Expand Down Expand Up @@ -127,7 +128,7 @@ class AddPreset : Subcommand("add", "add entry") {
}.getOrThrow()
}

private suspend fun addTopics(presetValue: PresetValue, parentDir: File) {
private suspend fun addTopics(presetValue: PresetValue, parentDir: File, tika: Tika) {
Napier.i {
"topics count ${presetValue.topicData?.size}"
}
Expand Down Expand Up @@ -392,7 +393,7 @@ class AddPreset : Subcommand("add", "add entry") {
return content
}

private suspend fun addUsers(presetValue: PresetValue, parentDir: File?) {
private suspend fun addUsers(presetValue: PresetValue, parentDir: File?, tika: Tika) {
val userList = presetValue.userData ?: return
Napier.i {
"users count ${presetValue.userData?.size}"
Expand All @@ -408,7 +409,7 @@ class AddPreset : Subcommand("add", "add entry") {
} else {
val path = File(parentDir, icon)
val p = "$id/avatar.${path.extension}"
backend.mediaService.upload(AMEDIA_BUCKET, listOf(UploadPack(p, path)))
uploadOneFil(path, tika, backend, "avatar.${path.extension}", id, null).getOrThrow()
Tuple5(it, p, derPublicKey, ad, id)
}
}
Expand All @@ -429,7 +430,7 @@ class AddPreset : Subcommand("add", "add entry") {
}.getOrThrow()
}

private suspend fun addCommunity(presetValue: PresetValue, parentDir: File?) {
private suspend fun addCommunity(presetValue: PresetValue, parentDir: File?, tika: Tika) {
val communityData = presetValue.communityData!!
Napier.i {
"communities count ${presetValue.communityData?.size}"
Expand All @@ -442,7 +443,7 @@ class AddPreset : Subcommand("add", "add entry") {
} else {
val path = File(parentDir, icon)
val p = "$id/community-icon.${path.extension}"
backend.mediaService.upload(AMEDIA_BUCKET, listOf(UploadPack(p, path)))
uploadOneFil(path, tika, backend, "community-icon.${path.extension}", id, null).getOrThrow()
Triple(it, p, id)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,21 @@ class ClientCustomAuthProvider : AuthProvider {

override fun isApplicable(auth: HttpAuthHeader): Boolean {
Napier.v("isApplicable $auth", tag = "ClientAuth")
if (auth is HttpAuthHeader.Single) {
if (auth.authScheme == "Custom" && auth is HttpAuthHeader.Single) {
val data = auth.blob
val localData = LoginViewModel.session?.first
if (data != localData) {
LoginViewModel.updateSession(data, null)
}
return true
}
return auth.authScheme == "Custom" && auth is HttpAuthHeader.Single
return false
}

override suspend fun refreshToken(response: HttpResponse): Boolean {
val state = LoginViewModel.state.value as? ClientSession.SignUpSuccess
val data = LoginViewModel.session?.first
Napier.v("refreshToken $data", tag = "ClientAuth")
Napier.v("refreshToken $data ${state != null}", tag = "ClientAuth")
return if (state == null || data == null) {
false
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.media3.ui.PlayerView
import com.dokar.sonner.Toaster
import com.dokar.sonner.ToasterState
import com.dokar.sonner.rememberToasterState
import com.storyteller_f.a.app.LocalToaster
import io.github.aakira.napier.Napier
import io.github.aakira.napier.log
import kotlinx.coroutines.CoroutineScope
Expand All @@ -34,8 +35,7 @@ actual fun VideoView(modifier: Modifier, url: String, contentType: String) {
log {
"Video $url"
}
val toasterState = rememberToasterState()
Toaster(toasterState, alignment = Alignment.Center)
val toasterState = LocalToaster.current
val context = LocalContext.current
var size by remember {
mutableStateOf<VideoSize?>(null)
Expand Down
Loading

0 comments on commit 4ffdb97

Please sign in to comment.