From ac8770a5382c1356b390d40f8fb60d8ecb7e5d5d Mon Sep 17 00:00:00 2001 From: Maksim Pischulenok Date: Wed, 23 Jul 2025 23:39:16 +0300 Subject: [PATCH] Implement video conversion --- .../bot/handler/ThisCommandHandlerService.kt | 30 ++-- .../upload/DummyFileUploadService.kt | 16 --- .../memevizor/upload/FileUploaderService.kt | 22 ++- .../com/pischule/memevizor/upload/S3Config.kt | 2 - .../memevizor/upload/S3FileUploaderService.kt | 27 ---- .../pischule/memevizor/util/TelegramHelper.kt | 4 +- .../memevizor/video/VideoEncoderService.kt | 74 ++++++++++ src/main/resources/application.properties | 3 +- src/main/resources/static/index.html | 128 ++++++++++-------- 9 files changed, 188 insertions(+), 118 deletions(-) delete mode 100644 src/main/kotlin/com/pischule/memevizor/upload/DummyFileUploadService.kt delete mode 100644 src/main/kotlin/com/pischule/memevizor/upload/S3FileUploaderService.kt create mode 100644 src/main/kotlin/com/pischule/memevizor/video/VideoEncoderService.kt diff --git a/src/main/kotlin/com/pischule/memevizor/bot/handler/ThisCommandHandlerService.kt b/src/main/kotlin/com/pischule/memevizor/bot/handler/ThisCommandHandlerService.kt index f487ceb..de718dc 100644 --- a/src/main/kotlin/com/pischule/memevizor/bot/handler/ThisCommandHandlerService.kt +++ b/src/main/kotlin/com/pischule/memevizor/bot/handler/ThisCommandHandlerService.kt @@ -1,13 +1,14 @@ package com.pischule.memevizor.bot.handler import com.github.kotlintelegrambot.dispatcher.handlers.MessageHandlerEnvironment +import com.github.kotlintelegrambot.entities.ChatAction import com.github.kotlintelegrambot.entities.ChatId -import com.github.kotlintelegrambot.entities.Message import com.github.kotlintelegrambot.entities.reaction.ReactionType import com.pischule.memevizor.bot.BotProps import com.pischule.memevizor.upload.FileUploaderService import com.pischule.memevizor.util.getMaxResPhotoId import com.pischule.memevizor.util.getVideoFileId +import com.pischule.memevizor.video.VideoEncoderService import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.withLoggingContext import org.springframework.stereotype.Component @@ -18,6 +19,7 @@ private val logger = KotlinLogging.logger {} class ThisCommandHandlerService( private val botProps: BotProps, private val fileUploaderService: FileUploaderService, + private val videoEncoderService: VideoEncoderService, ) { private val confirmCommands = listOf("this", "!soxok") @@ -26,23 +28,35 @@ class ThisCommandHandlerService( val replyToMessage = env.message.replyToMessage ?: return - val (fileId, contentType) = getFileInfo(replyToMessage) ?: return + val imageFileId = replyToMessage.getMaxResPhotoId() + val videoFileId = replyToMessage.getVideoFileId() + val fileId = imageFileId ?: videoFileId ?: return + env.bot.sendChatAction(ChatId.fromId(env.message.chat.id), ChatAction.TYPING) withLoggingContext("file_id" to fileId) { val fileBytes = env.bot.downloadFileBytes(fileId) ?: return logger.info { "Downloaded a file from Telegram, size=${fileBytes.size}" } - fileUploaderService.uploadFile(fileBytes, "_", contentType) + val convertedBytes = + if (videoFileId != null) { + videoEncoderService.encodeToWebm(fileBytes) + } else { + fileBytes + } + + val contentType = + if (videoFileId != null) { + "video/webm" + } else { + "image/jpeg" + } + + fileUploaderService.uploadFile(convertedBytes, "_", contentType) reactToMessage(env, "πŸ‘") } } - private fun getFileInfo(message: Message): Pair? { - return message.getMaxResPhotoId()?.let { it to "image/jpeg" } - ?: message.getVideoFileId()?.let { it to "video/mp4" } - } - private fun shouldHandleMessage(env: MessageHandlerEnvironment): Boolean { val isApprover = env.message.from?.id?.let { botProps.approverUserIds.contains(it) } == true val command = env.message.text?.lowercase() diff --git a/src/main/kotlin/com/pischule/memevizor/upload/DummyFileUploadService.kt b/src/main/kotlin/com/pischule/memevizor/upload/DummyFileUploadService.kt deleted file mode 100644 index 41edef9..0000000 --- a/src/main/kotlin/com/pischule/memevizor/upload/DummyFileUploadService.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.pischule.memevizor.upload - -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.context.annotation.Profile -import org.springframework.stereotype.Service - -private val logger = KotlinLogging.logger {} - -@Profile("local") -@Service -class DummyFileUploadService() : FileUploaderService { - - override fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) { - logger.info { "File $filename has been successfully uploaded to nowhere" } - } -} diff --git a/src/main/kotlin/com/pischule/memevizor/upload/FileUploaderService.kt b/src/main/kotlin/com/pischule/memevizor/upload/FileUploaderService.kt index 39d7f51..e8a4cb7 100644 --- a/src/main/kotlin/com/pischule/memevizor/upload/FileUploaderService.kt +++ b/src/main/kotlin/com/pischule/memevizor/upload/FileUploaderService.kt @@ -1,8 +1,26 @@ package com.pischule.memevizor.upload +import io.github.oshai.kotlinlogging.KotlinLogging +import io.minio.MinioClient +import io.minio.PutObjectArgs +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean import org.springframework.stereotype.Service +private val logger = KotlinLogging.logger {} + +@ConditionalOnBean(S3Config::class) @Service -interface FileUploaderService { - fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) +class FileUploaderService(private val s3Client: MinioClient, private val s3Props: S3Props) { + fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) { + logger.info { "Before upload" } + s3Client.putObject( + PutObjectArgs.builder() + .bucket(s3Props.bucket) + .`object`(filename) + .stream(fileBytes.inputStream(), fileBytes.size.toLong(), -1) + .contentType(contentType) + .build() + ) + logger.info { "File $filename has been uploaded to S3" } + } } diff --git a/src/main/kotlin/com/pischule/memevizor/upload/S3Config.kt b/src/main/kotlin/com/pischule/memevizor/upload/S3Config.kt index 983efaf..6feb746 100644 --- a/src/main/kotlin/com/pischule/memevizor/upload/S3Config.kt +++ b/src/main/kotlin/com/pischule/memevizor/upload/S3Config.kt @@ -4,9 +4,7 @@ import io.minio.MinioClient import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile -@Profile("!local") @EnableConfigurationProperties(S3Props::class) @Configuration class S3Config { diff --git a/src/main/kotlin/com/pischule/memevizor/upload/S3FileUploaderService.kt b/src/main/kotlin/com/pischule/memevizor/upload/S3FileUploaderService.kt deleted file mode 100644 index 49d3636..0000000 --- a/src/main/kotlin/com/pischule/memevizor/upload/S3FileUploaderService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.pischule.memevizor.upload - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.minio.MinioClient -import io.minio.PutObjectArgs -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.stereotype.Service - -private val logger = KotlinLogging.logger {} - -@ConditionalOnBean(S3Config::class) -@Service -class S3FileUploaderService(private val s3Client: MinioClient, private val s3Props: S3Props) : - FileUploaderService { - override fun uploadFile(fileBytes: ByteArray, filename: String, contentType: String) { - logger.info { "Before upload" } - s3Client.putObject( - PutObjectArgs.builder() - .bucket(s3Props.bucket) - .`object`(filename) - .stream(fileBytes.inputStream(), fileBytes.size.toLong(), -1) - .contentType(contentType) - .build() - ) - logger.info { "File $filename has been uploaded to S3" } - } -} diff --git a/src/main/kotlin/com/pischule/memevizor/util/TelegramHelper.kt b/src/main/kotlin/com/pischule/memevizor/util/TelegramHelper.kt index b51af91..04765a5 100644 --- a/src/main/kotlin/com/pischule/memevizor/util/TelegramHelper.kt +++ b/src/main/kotlin/com/pischule/memevizor/util/TelegramHelper.kt @@ -2,6 +2,6 @@ package com.pischule.memevizor.util import com.github.kotlintelegrambot.entities.Message -fun Message.getMaxResPhotoId(): String? = this.photo?.lastOrNull()?.fileId +fun Message.getMaxResPhotoId(): String? = photo?.lastOrNull()?.fileId -fun Message.getVideoFileId(): String? = this.video?.fileId +fun Message.getVideoFileId(): String? = video?.fileId ?: videoNote?.fileId diff --git a/src/main/kotlin/com/pischule/memevizor/video/VideoEncoderService.kt b/src/main/kotlin/com/pischule/memevizor/video/VideoEncoderService.kt new file mode 100644 index 0000000..f601792 --- /dev/null +++ b/src/main/kotlin/com/pischule/memevizor/video/VideoEncoderService.kt @@ -0,0 +1,74 @@ +package com.pischule.memevizor.video + +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlin.io.path.createTempFile +import kotlin.io.path.deleteIfExists +import kotlin.io.path.readBytes +import kotlin.io.path.writeBytes +import org.springframework.stereotype.Service + +private val logger = KotlinLogging.logger {} + +@Service +class VideoEncoderService { + + fun encodeToWebm(inputVideo: ByteArray): ByteArray { + logger.info { "Started encoding a file" } + + val inputFile = createTempFile() + val outputFile = createTempFile() + val command: List = + listOf( + "ffmpeg", + "-i", + "$inputFile", + // video + "-map", + "0:v", + "-c:v", + "libvpx-vp9", + "-cpu-used", + "6", + "-crf", + "40", + "-b:v", + "0k", + // audio + "-map", + "0:a", + "-c:a", + "libopus", + "-b:a", + "96k", + "-vbr", + "on", + "-compression_level", + "10", + // file + "-f", + "webm", + "-y", + "$outputFile", + ) + + try { + inputFile.writeBytes(inputVideo) + + val process = ProcessBuilder(command).redirectErrorStream(true).start() + + val output = process.inputStream.bufferedReader().use { it.readText() } + val exitCode = process.waitFor() + + if (exitCode != 0) { + error("Video conversion failed: exit code $exitCode, $output") + } + + logger.info { "Converted" } + + return outputFile.readBytes() + } finally { + inputFile.deleteIfExists() + outputFile.deleteIfExists() + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 23fe2d9..9579573 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1 @@ -spring.application.name=memevizor -logging.structured.format.console=ecs \ No newline at end of file +spring.application.name=memevizor \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 8727eca..a74174f 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -3,116 +3,126 @@ - - memevizor + Dynamic Media Refresh -БмСшная ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° - + +
+ + +