diff --git a/src/Main.kt b/src/Main.kt index 2ae6cec..426288a 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -2,17 +2,20 @@ import audio.AudioPlayer import barix.BarixConnection import barix.TCP_Barix_Command_Server import codes.Somecodes.Companion.Get_ANN_ID +import codes.Somecodes.Companion.IsAlphabethic +import codes.Somecodes.Companion.IsNumber import codes.Somecodes.Companion.ValidFile +import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.dateformat1 import codes.Somecodes.Companion.timeformat2 import com.sun.jna.Platform +import content.Category import content.ContentCache import content.Language import content.ScheduleDay import content.VoiceType import database.MariaDB import database.Messagebank -import database.QueuePaging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -24,18 +27,19 @@ import web.WebApp import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalTime +import java.util.function.Consumer import kotlin.concurrent.fixedRateTimer fun main() { val version = "0.0.1 (23/09/2025)" - val StreamerOutputs : MutableMap = HashMap() + val StreamerOutputs: MutableMap = HashMap() if (Platform.isWindows()) { // supaya OSHI bisa mendapatkan CPU usage di Windows seperti di Task Manager GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true) } - Logger.info{"Starting AAS New Generation version $version"} + Logger.info { "Starting AAS New Generation version $version" } val audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer.InitAudio(1) val content = ContentCache() @@ -49,19 +53,19 @@ fun main() { * @return true jika semua valid, false jika ada yang tidak valid */ fun AllBroadcastZonesValid(bz: List): Boolean { - if (bz.isNotEmpty()){ + if (bz.isNotEmpty()) { val validchannels = bz // check apakah tiap zone ada di database broadcast zones - .filter { - z1 -> db.BroadcastZoneList.find { z2 -> z2.SoundChannel==z1 } != null + .filter { z1 -> + db.BroadcastZoneList.find { z2 -> z2.SoundChannel == z1 } != null } // check apakah tiap zone ada di SoundChannelList dan Online - .filter { - z3 -> StreamerOutputs.any { sc -> sc.value.channel==z3 && sc.value.isOnline() } + .filter { z3 -> + StreamerOutputs.any { sc -> sc.value.channel == z3 && sc.value.isOnline() } } // kalau jumlah valid channel sama dengan jumlah broadcast zone, berarti semua valid - return validchannels.size==bz.size + return validchannels.size == bz.size } return false } @@ -72,9 +76,9 @@ fun main() { * @return true jika semua idle, false jika ada yang tidak idle */ fun AllBroadcastZoneIdle(bz: List): Boolean { - if (bz.isNotEmpty()){ - return bz.all { - z1 -> StreamerOutputs.any { sc -> sc.value.channel==z1 && sc.value.isIdle() } + if (bz.isNotEmpty()) { + return bz.all { z1 -> + StreamerOutputs.any { sc -> sc.value.channel == z1 && sc.value.isIdle() } } } return false @@ -96,88 +100,258 @@ fun main() { /** * Fungsi untuk ambil messagebank berdasarkan ANN_ID, diurutkan berdasarkan urutan bahasa di urutan_bahasa * @param id ANN_ID dari messagebank + * @param languages List of language yang diinginkan, default urutan_bahasa * @return List of Messagebank */ - fun Get_MessageBank_by_id(id: Int) : ArrayList{ + fun Get_MessageBank_by_id(id: Int, languages: List = urutan_bahasa): ArrayList { val mb_list = ArrayList() - urutan_bahasa.forEach { - lang -> db.MessagebankList.find { mb -> mb.ANN_ID==id.toUInt() && mb.Language==lang && mb.Voice_Type==selected_voice }?.let { - mb_list.add(it) - } + languages.forEach { lang -> + db.MessagebankList.find { mb -> mb.ANN_ID == id.toUInt() && mb.Language == lang && mb.Voice_Type == selected_voice } + ?.let { + mb_list.add(it) + } } return mb_list } + /** + * Find soundbank files from messagebank tags, filtered by VoiceType and Language + * @param mb Messagebank object + * @param variables Map of variables to replace in tags. + * @param cbOK Callback function if success, returns List of soundbank file names + * @param cbFail Callback function if fail, returns error message + */ + fun Get_Soundbank_Files( + mb: Messagebank, + variables: Map, + cbOK: Consumer>, + cbFail: Consumer + ) { + val tags = mb.Message_TAGS.split(" ") + if (tags.isEmpty()) { + cbFail.accept("No tags found in messagebank id ${mb.ANN_ID}") + return + } + // dapatkan soundbank array berdasarkan VoiceType dan Language + val sb = db.SoundbankList + .filter { it.VoiceType == mb.Voice_Type } + .filter { it.Language == mb.Language } + if (sb.isEmpty()) { + cbFail.accept("No soundbank found for voice type ${mb.Voice_Type} and language ${mb.Language}") + return + } + + val files = mutableListOf() + + tags.forEach { tag -> + val _tag = tag.trim() + when (_tag) { + "[AIRPLANE_NAME]" -> { + + } + + "[FLIGHT_NUMBER]" -> { + + } + + "[PLATNOMOR]" -> { + + } + + "[CITY]" -> { + + } + + "[PLACES]" -> { + + } + + "[ETAD]" -> { + + } + + "[SHALAT]" -> { + + } + + "[BCB]" -> { + + } + + "[GATENUMBER]" -> { + // gate number bisa angka saja, misalnya 1,2,3 + // atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C + val value = variables["GATENUMBER"].orEmpty() + if (ValidString(value)) { + val values = value.split(",").map { it.trim() }.filter { ValidString(it) } + if (values.isNotEmpty()){ + values.forEach { + if (IsNumber(it)){ + // gate number hanya angka + } else { + // gate number gabungan huruf dan angka + val regex = Regex("([A-Z])?(\\d+)([A-Z])?") + + } + } + + } else { + cbFail.accept("GATENUMBER variable is empty after split for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + } else { + cbFail.accept("GATENUMBER variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[REASON]" -> { + val reason = sb.firstOrNull { it.Category == Category.Reason.name && it.TAG == _tag } + if (reason != null) { + if (ValidFile(reason.Path)) { + files.add(reason.Path) + } else { + cbFail.accept("Invalid soundbank file ${reason.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Reason found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } + + "[PROCEDURE]" -> { + val procedure = sb.firstOrNull { it.Category == Category.Procedure.name && it.TAG == _tag } + if (procedure != null) { + if (ValidFile(procedure.Path)) { + files.add(procedure.Path) + } else { + cbFail.accept("Invalid soundbank file ${procedure.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Procedure found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } + + else -> { + // Phrase + val phrase = sb.firstOrNull { it.Category == Category.Phrase.name && it.TAG == _tag } + if (phrase != null) { + if (ValidFile(phrase.Path)) { + files.add(phrase.Path) + } else { + cbFail.accept("Invalid soundbank file ${phrase.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Phrase found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + + } + + } + } + + } + jobloop@ while (isActive) { delay(1000) // prioritas 1 , habisin queue paging - for(it in db.Read_Queue_Paging()){ - if (it.BroadcastZones.isNotBlank()){ - val zz = it.BroadcastZones.split(";") - if (AllBroadcastZonesValid(zz)){ - if (AllBroadcastZoneIdle(zz)){ - if (it.Source=="PAGING"){ + for (qp in db.Read_Queue_Paging()) { + if (qp.BroadcastZones.isNotBlank()) { + val zz = qp.BroadcastZones.split(";") + if (AllBroadcastZonesValid(zz)) { + if (AllBroadcastZoneIdle(zz)) { + if (qp.Source == "PAGING") { // nama file ada di Message - if (ValidFile(it.Message)){ - val afi = audioPlayer.LoadAudioFile(it.Message) - zz.forEach { - z1 -> StreamerOutputs.values.find { it.channel==z1 }?.SendData(afi.bytes, {db.Add_Log("AAS", it) }, {db.Add_Log("AAS", it)} ) + if (ValidFile(qp.Message)) { + val afi = audioPlayer.LoadAudioFile(qp.Message) + zz.forEach { z1 -> + StreamerOutputs.values.find { it.channel == z1 } + ?.SendData(afi.bytes, { db.Add_Log("AAS", it) }, { db.Add_Log("AAS", it) }) } - val logmessage = "Broadcast started PAGING with Filename '${it.Message}' to zones: ${it.BroadcastZones}" - Logger.info { logmessage} + val logmessage = + "Broadcast started PAGING with Filename '${qp.Message}' to zones: ${qp.BroadcastZones}" + Logger.info { logmessage } db.Add_Log("AAS", logmessage) - db.Delete_Queue_Paging_by_index(it.index) + db.Delete_Queue_Paging_by_index(qp.index) continue@jobloop } else { // file tidak valid, delete from queue paging - db.Delete_Queue_Paging_by_index(it.index) - db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to invalid audio file" ) + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log( + "AAS", + "Cancelled paging message with index ${qp.index} due to invalid audio file" + ) } - } else if (it.Source=="SHALAT"){ - val ann_id = Get_ANN_ID(it.Message) - if (ann_id>0){ - Get_MessageBank_by_id(ann_id).forEach { - // cari tags nya, create content nya, broadcast ke semua zone + } else if (qp.Source == "SHALAT") { + val ann_id = Get_ANN_ID(qp.Message) + if (ann_id > 0) { + // shalat, ambil messagebank berdasarkan ann_id dengan bahasa Indonesia saja + Get_MessageBank_by_id(ann_id, listOf(Language.INDONESIA.name)).let { mblist -> + if (mblist.isNotEmpty()) { + //TODO find soundbank + } else { + // tidak ada messagebank dengan ann_id ini, delete from queue paging + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log( + "AAS", + "Cancelled Shalat message with index ${qp.index} due to ANN_ID $ann_id not found in Messagebank" + ) + } } - } else{ + } else { // invalid ann_id, delete from queue paging - db.Delete_Queue_Paging_by_index(it.index) - db.Add_Log("AAS", "Cancelled Shalat message with index ${it.index} due to invalid ANN_ID" ) + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log( + "AAS", + "Cancelled Shalat message with index ${qp.index} due to invalid ANN_ID" + ) } } } } else { // ada broadcast zone yang tidak valid, delete from queue paging - db.Delete_Queue_Paging_by_index(it.index) - db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to invalid broadcast zone" ) + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log( + "AAS", + "Cancelled paging message with index ${qp.index} due to invalid broadcast zone" + ) } } else { // invalid broadcast zone, delete from queue paging - db.Delete_Queue_Paging_by_index(it.index) - db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to empty broadcast zone") + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to empty broadcast zone") } } // prioritas 2, habisin queue table db.Read_Queue_Table().forEach { - if (it.BroadcastZones.isNotEmpty()){ + if (it.BroadcastZones.isNotEmpty()) { val zz = it.BroadcastZones.split(";") - if (AllBroadcastZonesValid(zz)){ - if (AllBroadcastZoneIdle(zz)){ - if (it.Type=="SOUNDBANK"){ + if (AllBroadcastZonesValid(zz)) { + if (AllBroadcastZoneIdle(zz)) { + if (it.Type == "SOUNDBANK") { - } else if (it.Type=="TIMER"){ + } else if (it.Type == "TIMER") { - } + } } } else { // ada broadcast zone yang tidak valid, delete from queue table db.Delete_Queue_Table_by_index(it.index) - db.Add_Log("AAS", "Cancelled table message with index ${it.index} due to invalid broadcast zone") + db.Add_Log( + "AAS", + "Cancelled table message with index ${it.index} due to invalid broadcast zone" + ) } } else { // invalid broadcast zone, delete from queue table @@ -195,10 +369,10 @@ fun main() { delay(1000) val localtime = LocalTime.now() // detik harus 00 - if (localtime.second!=0) continue + if (localtime.second != 0) continue val timestring = timeformat2.format(localtime) - val sch = db.SchedulebankList.filter{ - it.Time==timestring && it.Enable + val sch = db.SchedulebankList.filter { + it.Time == timestring && it.Enable } // tidak ada schedule dengan time sekarang dan enable=true if (sch.isEmpty()) continue @@ -207,15 +381,15 @@ fun main() { val ddmmyyyy = dateformat1.format(localdate) // check special date dulu val specialdate = sch.find { - it.Day==ddmmyyyy + it.Day == ddmmyyyy } - if (specialdate!=null) { + if (specialdate != null) { // TODO Masukin ke queue table sebagai schedule special date } // cek weekly schedule val weekly = sch.find { - it.Day == when(localdate.dayOfWeek){ + it.Day == when (localdate.dayOfWeek) { DayOfWeek.MONDAY -> ScheduleDay.Monday.name DayOfWeek.TUESDAY -> ScheduleDay.Tuesday.name DayOfWeek.WEDNESDAY -> ScheduleDay.Wednesday.name @@ -225,7 +399,7 @@ fun main() { DayOfWeek.SUNDAY -> ScheduleDay.Sunday.name } } - if (weekly!=null) { + if (weekly != null) { // TODO Masukin ke queue table sebagai schedule weekly } @@ -233,13 +407,12 @@ fun main() { val daily = sch.find { it.Day == ScheduleDay.Everyday.name } - if (daily!=null) { + if (daily != null) { // TODO Masukin ke queue table sebagai schedule daily } - } } @@ -252,16 +425,16 @@ fun main() { ) web.Start() - val barixserver = TCP_Barix_Command_Server () + val barixserver = TCP_Barix_Command_Server() barixserver.StartTcpServer { cmd -> - Logger.info{cmd} + Logger.info { cmd } val _streamer = StreamerOutputs[cmd.ipaddress] val _sc = db.SoundChannelList.find { it.ip == cmd.ipaddress } - if (_streamer==null){ + if (_streamer == null) { // belum create BarixConnection untuk ipaddress ini - Logger.info{"New Streamer Output connection from ${cmd.ipaddress}"} - if (_sc!=null){ - val _bc = BarixConnection(_sc.index,_sc.channel,cmd.ipaddress) + Logger.info { "New Streamer Output connection from ${cmd.ipaddress}" } + if (_sc != null) { + val _bc = BarixConnection(_sc.index, _sc.channel, cmd.ipaddress) _bc.vu = cmd.vu _bc.bufferRemain = cmd.buffremain _bc.statusData = cmd.statusdata @@ -270,7 +443,7 @@ fun main() { } else { // sudah ada, update data - if (_sc !=null && _sc.channel != _streamer.channel) { + if (_sc != null && _sc.channel != _streamer.channel) { _streamer.channel = _sc.channel } _streamer.vu = cmd.vu @@ -280,7 +453,7 @@ fun main() { } - val onlinechecker = fixedRateTimer(name="onlinecheck", initialDelay = 1000, period = 1000) { + val onlinechecker = fixedRateTimer(name = "onlinecheck", initialDelay = 1000, period = 1000) { // cek setiap 1 detik, decrement online counter semua BarixConnection StreamerOutputs.values.forEach { it.decrementOnlineCounter() @@ -289,14 +462,14 @@ fun main() { // shutdown hook Runtime.getRuntime().addShutdownHook(Thread { - Logger.info{"Shutdown hook called, stopping services..."} + Logger.info { "Shutdown hook called, stopping services..." } barixserver.StopTcpCommand() onlinechecker.cancel() web.Stop() audioPlayer.Close() db.close() - Logger.info{"All services stopped, exiting application."} - } ) + Logger.info { "All services stopped, exiting application." } + }) } diff --git a/src/codes/Somecodes.kt b/src/codes/Somecodes.kt index 7b84e00..db6e0c0 100644 --- a/src/codes/Somecodes.kt +++ b/src/codes/Somecodes.kt @@ -38,6 +38,20 @@ class Somecodes { // regex for getting ann_id from Message, which is the number inside [] private val ann_id_regex = Regex("\\[(\\d+)]") + /** + * Check if a string is a valid number. + */ + fun IsNumber(value: String) : Boolean { + return value.toIntOrNull() != null + } + + /** + * Check if a string is alphabetic (contains only letters). + */ + fun IsAlphabethic(value: String) : Boolean { + return value.all { it.isLetter() } + } + /** * Extract ANN ID from a message string. * The ANN ID is expected to be a number enclosed in square brackets (e.g., "[123]"). diff --git a/src/content/Category.kt b/src/content/Category.kt index 7ee74ae..d011bda 100644 --- a/src/content/Category.kt +++ b/src/content/Category.kt @@ -11,5 +11,7 @@ enum class Category(name: String) { PlatNomor("PlatNomor"), Shalat("Shalat"), Year("Year"), - Birthday("Birthday"); + Birthday("Birthday"), + Reason("Reason"), + Procedure("Procedure"); } \ No newline at end of file