commit 06/11/2025

This commit is contained in:
2025-11-06 11:31:02 +07:00
parent 72c509feec
commit b24c153615
34 changed files with 719 additions and 174 deletions

View File

@@ -344,6 +344,16 @@ th, td {
color: #fff; color: #fff;
} }
.btn-disable {
color: white;
background-color: var(--bs-red);
}
.btn-enable {
color: white;
background-color: #36d636;
}
.btn-logout { .btn-logout {
background-color: #4280ab; background-color: #4280ab;
color: #fff; color: #fff;

View File

@@ -28,8 +28,7 @@ let $input_gatenumber = null;
let $input_flightnumber = null; let $input_flightnumber = null;
let $input_licenseplate = null; let $input_licenseplate = null;
let $input_conveyorbelt = null; let $input_conveyorbelt = null;
let $input_hours = null; let $input_etad = null;
let $input_minutes = null;
let $row_airplane = null; let $row_airplane = null;
let $row_city = null; let $row_city = null;
@@ -42,6 +41,11 @@ let $col_conveyorbelt = null;
let $col_procedure = null; let $col_procedure = null;
let $col_licenseplate = null; let $col_licenseplate = null;
/**
* @type {message[]}
*/
let selected_messages = [];
/** /**
* @typedef {Object} message * @typedef {Object} message
* @property {number} id - The ID of the message * @property {number} id - The ID of the message
@@ -438,8 +442,8 @@ function reload_database() {
}); });
} }
function empty_preview(){ function empty_preview() {
$preview_arabic.empty(); $preview_arabic.empty();
$preview_chinese.empty(); $preview_chinese.empty();
$preview_english.empty(); $preview_english.empty();
$preview_indonesia.empty(); $preview_indonesia.empty();
@@ -455,7 +459,7 @@ function empty_preview(){
} }
function enable_disable_fields(){ function enable_disable_fields() {
$row_airplane.hide(); $row_airplane.hide();
$row_city.hide(); $row_city.hide();
$row_gatenumber.hide(); $row_gatenumber.hide();
@@ -469,10 +473,10 @@ function enable_disable_fields(){
// show airplane row if English preview contains the placeholder // show airplane row if English preview contains the placeholder
const text = $preview_english.text() || ""; const text = $preview_english.text() || "";
if (text.indexOf('[CITY]') !== -1) $row_city.show(); if (text.indexOf('[CITY]') !== -1) $row_city.show();
if (text.indexOf('[AIRPLANE_NAME]') !== -1) $row_airplane.show(); if (text.indexOf('[AIRPLANE_NAME]') !== -1) $row_airplane.show();
if (text.indexOf('[FLIGHT_NUMBER]') !== -1) $row_airplane.show(); if (text.indexOf('[FLIGHT_NUMBER]') !== -1) $row_airplane.show();
if (text.indexOf('[GATENUMBER]') !== -1) $row_gatenumber.show(); if (text.indexOf('[GATENUMBER]') !== -1) $row_gatenumber.show();
if (text.indexOf('[ETAD]') !== -1) $row_time.show(); if (text.indexOf('[ETAD]') !== -1) $row_time.show();
if (text.indexOf('[REASON]') !== -1) $col_reason.show(); if (text.indexOf('[REASON]') !== -1) $col_reason.show();
if (text.indexOf('[PLACES]') !== -1) $col_places.show(); if (text.indexOf('[PLACES]') !== -1) $col_places.show();
@@ -482,76 +486,140 @@ function enable_disable_fields(){
if (text.indexOf('[PLATNOMOR]') !== -1) $col_licenseplate.show(); if (text.indexOf('[PLATNOMOR]') !== -1) $col_licenseplate.show();
} }
function update_preview(language){ function ValidString(str) {
return (str != null && typeof str === 'string' && str.trim().length > 0);
}
function update_preview(language) {
let text = ""; let text = "";
let $preview = null; let $preview = null;
if (language === "indonesia") { if (language === "indonesia") {
$preview = $preview_indonesia; $preview = $preview_indonesia;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "indonesia")?.message_details || "";
} else if (language === "english") { } else if (language === "english") {
$preview = $preview_english; $preview = $preview_english;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "english")?.message_details || "";
} else if (language === "chinese") { } else if (language === "chinese") {
$preview = $preview_chinese; $preview = $preview_chinese;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "chinese")?.message_details || "";
} else if (language === "japanese") { } else if (language === "japanese") {
$preview = $preview_japanese; $preview = $preview_japanese;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "japanese")?.message_details || "";
} else if (language === "arabic") { } else if (language === "arabic") {
$preview = $preview_arabic; $preview = $preview_arabic;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "arabic")?.message_details || "";
} else if (language === "local") { } else if (language === "local") {
$preview = $preview_local; $preview = $preview_local;
text = selected_messages.find(m => (m.language || "").toLowerCase() === "local")?.message_details || "";
} }
if ($preview) {
text = $preview.text();
}
if (text.indexOf('[CITY]') !== -1) { if (text.indexOf('[CITY]') !== -1) {
let cities = $select_city.val(); let cities = $select_city.val();
if (Array.isArray(cities)) { if (Array.isArray(cities) && cities.length > 0) {
text = text.replace(/\[CITY\]/g, cities.join(", ")); let citiesNames = [];
} else { for (let cityTag of cities) {
text = text.replace(/\[CITY\]/g, cities || ""); let ct = window.semiautodata.cities.find(c => c.tag === cityTag);
if (ct) {
citiesNames.push(ct.value);
}
}
if (citiesNames.length > 0) {
text = text.replace(/\[CITY\]/g, citiesNames.join(", "));
}
} }
} }
if (text.indexOf('[AIRPLANE_NAME]') !== -1) { if (text.indexOf('[AIRPLANE_NAME]') !== -1) {
let airlineTag = $select_airline.val() || ""; let airlineTag = $select_airline.val();
let airlineObj = window.semiautodata.airlines.find(a => a.tag === airlineTag); if (ValidString(airlineTag)) {
let airlineName = airlineObj ? airlineObj.value : ""; let airlineObj = window.semiautodata.airlines.find(a => a.tag === airlineTag);
text = text.replace(/\[AIRPLANE_NAME\]/g, airlineName); let airlineName = airlineObj ? airlineObj.value : "";
if (airlineName.length > 0) {
text = text.replace(/\[AIRPLANE_NAME\]/g, airlineName);
}
}
} }
if (text.indexOf('[FLIGHT_NUMBER]') !== -1) { if (text.indexOf('[FLIGHT_NUMBER]') !== -1) {
let flightNumber = $input_flightnumber.val() || ""; let airlineTag = $select_airline.val();
text = text.replace(/\[FLIGHT_NUMBER\]/g, flightNumber); let flightNumber = $input_flightnumber.val();
if (ValidString(airlineTag) && ValidString(flightNumber)) {
text = text.replace(/\[FLIGHT_NUMBER\]/g, `${airlineTag} ${flightNumber}`);
}
} }
if (text.indexOf('[GATENUMBER]') !== -1) { if (text.indexOf('[GATENUMBER]') !== -1) {
let gateNumber = $input_gatenumber.val() || ""; let gateNumber = $input_gatenumber.val();
text = text.replace(/\[GATENUMBER\]/g, gateNumber); if (ValidString(gateNumber)) {
text = text.replace(/\[GATENUMBER\]/g, gateNumber);
}
} }
if (text.indexOf('[ETAD]') !== -1) { if (text.indexOf('[ETAD]') !== -1) {
let etad = $input_etad.val() || ""; let etad = $input_etad.val();
text = text.replace(/\[ETAD\]/g, etad); if (ValidString(etad)) {
text = text.replace(/\[ETAD\]/g, etad);
}
} }
if (text.indexOf('[REASON]') !== -1) { if (text.indexOf('[REASON]') !== -1) {
let reason = $select_reason.val() || ""; let reason = $select_reason.val();
text = text.replace(/\[REASON\]/g, reason); if (ValidString(reason)) {
let reasonobj = window.semiautodata.reasons.find(r => r.tag === reason);
let reasontext = reasonobj ? reasonobj.value : "";
if (ValidString(reasontext)) {
text = text.replace(/\[REASON\]/g, reasontext);
}
}
} }
if (text.indexOf('[PLACES]') !== -1) { if (text.indexOf('[PLACES]') !== -1) {
let placeTag = $select_places.val() || ""; let placeTag = $select_places.val();
text = text.replace(/\[PLACES\]/g, placeTag); if (ValidString(placeTag)) {
let placeobj = window.semiautodata.places.find(p => p.tag === placeTag);
let placetext = placeobj ? placeobj.value : "";
if (ValidString(placetext)) {
text = text.replace(/\[PLACES\]/g, placetext);
}
}
} }
if (text.indexOf('[SHALAT]') !== -1) { if (text.indexOf('[SHALAT]') !== -1) {
let shalatTag = $select_shalat.val() || ""; let shalatTag = $select_shalat.val();
text = text.replace(/\[SHALAT\]/g, shalatTag); if (ValidString(shalatTag)) {
let shalatobj = window.semiautodata.shalat.find(s => s.tag === shalatTag);
let shalattext = shalatobj ? shalatobj.value : "";
if (ValidString(shalattext)) {
text = text.replace(/\[SHALAT\]/g, shalattext);
}
}
} }
if (text.indexOf('[BCB]') !== -1) { if (text.indexOf('[BCB]') !== -1) {
let bcbTag = $select_bcb.val() || ""; let bcbTag = $select_bcb.val();
text = text.replace(/\[BCB\]/g, bcbTag); if (ValidString(bcbTag)) {
text = text.replace(/\[BCB\]/g, bcbTag);
}
} }
if (text.indexOf('[PROCEDURE]') !== -1) { if (text.indexOf('[PROCEDURE]') !== -1) {
let procedureTag = $select_procedure.val() || ""; let procedureTag = $select_procedure.val();
text = text.replace(/\[PROCEDURE\]/g, procedureTag); if (ValidString(procedureTag)) {
let procedureobj = window.semiautodata.procedures.find(p => p.tag === procedureTag);
let proceduretext = procedureobj ? procedureobj.value : "";
if (ValidString(proceduretext)) {
text = text.replace(/\[PROCEDURE\]/g, proceduretext);
}
}
} }
if (text.indexOf('[PLATNOMOR]') !== -1) { if (text.indexOf('[PLATNOMOR]') !== -1) {
let platNomorTag = $select_licenseplate.val() || ""; let platNomorTag = $select_licenseplate.val();
text = text.replace(/\[PLATNOMOR\]/g, platNomorTag); if (ValidString(platNomorTag)) {
text = text.replace(/\[PLATNOMOR\]/g, platNomorTag);
}
}
if (text.indexOf('[SEQUENCE]') !== -1) {
let sequenceTag = $select_sequence.val();
if (ValidString(sequenceTag)) {
let sequenceobj = window.semiautodata.sequences.find(s => s.tag === sequenceTag);
let sequencetext = sequenceobj ? sequenceobj.value : "";
if (ValidString(sequencetext)) {
text = text.replace(/\[SEQUENCE\]/g, sequencetext);
}
}
} }
if ($preview) { if ($preview) {
@@ -559,13 +627,45 @@ function update_preview(language){
} }
} }
function update_all_previews(){ function isComplete(chkbox, preview) {
if (chkbox.is(":checked")) {
const text = preview.text();
if (text.indexOf('[') !== -1) {
return false;
}
if (text.indexOf(']') !== -1) {
return false;
}
}
return true;
}
function check_complete_message() {
let complete = true;
if (!isComplete($enable_indonesia, $preview_indonesia)) complete = false;
if (!isComplete($enable_english, $preview_english)) complete = false;
if (!isComplete($enable_chinese, $preview_chinese)) complete = false;
if (!isComplete($enable_japanese, $preview_japanese)) complete = false;
if (!isComplete($enable_arabic, $preview_arabic)) complete = false;
if (!isComplete($enable_local, $preview_local)) complete = false;
if (complete) {
$("#send_broadcast").removeClass("btn-disable").addClass("btn-enable");
} else {
$("#send_broadcast").removeClass("btn-enable").addClass("btn-disable");
}
}
function update_all_previews() {
update_preview("indonesia"); update_preview("indonesia");
update_preview("english"); update_preview("english");
update_preview("chinese"); update_preview("chinese");
update_preview("japanese"); update_preview("japanese");
update_preview("arabic"); update_preview("arabic");
update_preview("local"); update_preview("local");
check_complete_message();
} }
function fill_items() { function fill_items() {
@@ -584,8 +684,7 @@ function fill_items() {
$input_flightnumber.empty(); $input_flightnumber.empty();
$input_licenseplate.empty(); $input_licenseplate.empty();
$input_conveyorbelt.empty(); $input_conveyorbelt.empty();
$input_hours.empty(); $input_etad.empty();
$input_minutes.empty();
empty_preview(); empty_preview();
@@ -623,17 +722,17 @@ function fill_items() {
$enable_indonesia.prop("checked", !!checked); $enable_indonesia.prop("checked", !!checked);
return true; return true;
} }
if (l.startsWith("english") ) { if (l.startsWith("english")) {
if (checked) $preview_english.text(value); else $preview_english.empty(); if (checked) $preview_english.text(value); else $preview_english.empty();
$enable_english.prop("checked", !!checked); $enable_english.prop("checked", !!checked);
return true; return true;
} }
if (l.startsWith("chinese") ) { if (l.startsWith("chinese")) {
if (checked) $preview_chinese.text(value); else $preview_chinese.empty(); if (checked) $preview_chinese.text(value); else $preview_chinese.empty();
$enable_chinese.prop("checked", !!checked); $enable_chinese.prop("checked", !!checked);
return true; return true;
} }
if (l.startsWith("japanese") ) { if (l.startsWith("japanese")) {
if (checked) $preview_japanese.text(value); else $preview_japanese.empty(); if (checked) $preview_japanese.text(value); else $preview_japanese.empty();
$enable_japanese.prop("checked", !!checked); $enable_japanese.prop("checked", !!checked);
return true; return true;
@@ -660,6 +759,7 @@ function fill_items() {
} }
} }
selected_messages = [];
if (willSelect) { if (willSelect) {
// unselect other selected rows and clear their previews // unselect other selected rows and clear their previews
$tbody_message.find("tr.table-active").each(function () { $tbody_message.find("tr.table-active").each(function () {
@@ -676,8 +776,10 @@ function fill_items() {
const sameMsgs = window.semiautodata.messages.filter(m => Number(m.id) === Number(groupId)); const sameMsgs = window.semiautodata.messages.filter(m => Number(m.id) === Number(groupId));
for (let m of sameMsgs) { for (let m of sameMsgs) {
applyLang(m.language, m.message_details, true); applyLang(m.language, m.message_details, true);
selected_messages.push(m);
} }
enable_disable_fields(); enable_disable_fields();
update_all_previews();
} }
} else { } else {
// deselect this row and clear its previews // deselect this row and clear its previews
@@ -742,7 +844,7 @@ function fill_items() {
placeholder: "Select a place", placeholder: "Select a place",
width: '100%', width: '100%',
multiple: false, multiple: false,
data: window.semiautodata.places.map(p => ({ id: p.tag, text: p.value })) data: window.semiautodata.places.map(p => ({ id: p.tag, text: p.value + " [" + p.tag + "]" }))
}); });
$select_places.val(null).trigger("change"); $select_places.val(null).trigger("change");
@@ -756,7 +858,7 @@ function fill_items() {
placeholder: "Select shalat time", placeholder: "Select shalat time",
width: '100%', width: '100%',
multiple: false, multiple: false,
data: window.semiautodata.shalat.map(s => ({ id: s.tag, text: s.value })) data: window.semiautodata.shalat.map(s => ({ id: s.tag, text: s.value + " [" + s.tag + "]" }))
}); });
$select_shalat.val(null).trigger("change"); $select_shalat.val(null).trigger("change");
} }
@@ -765,7 +867,7 @@ function fill_items() {
placeholder: "Select a reason", placeholder: "Select a reason",
width: '100%', width: '100%',
multiple: false, multiple: false,
data: window.semiautodata.reasons.map(r => ({ id: r.tag, text: r.value })) data: window.semiautodata.reasons.map(r => ({ id: r.tag, text: r.value + " [" + r.tag + "]" }))
}); });
$select_reason.val(null).trigger("change"); $select_reason.val(null).trigger("change");
$select_reason.on('change', function () { $select_reason.on('change', function () {
@@ -778,7 +880,7 @@ function fill_items() {
placeholder: "Select compensation", placeholder: "Select compensation",
width: '100%', width: '100%',
multiple: false, multiple: false,
data: window.semiautodata.compensation.map(c => ({ id: c.tag, text: c.value })) data: window.semiautodata.compensation.map(c => ({ id: c.tag, text: c.value + " [" + c.tag + "]" }))
}); });
$select_compensation.val(null).trigger("change"); $select_compensation.val(null).trigger("change");
$select_compensation.on('change', function () { $select_compensation.on('change', function () {
@@ -790,7 +892,7 @@ function fill_items() {
placeholder: "Select procedure", placeholder: "Select procedure",
width: '100%', width: '100%',
multiple: false, multiple: false,
data: window.semiautodata.procedures.map(p => ({ id: p.tag, text: p.value })) data: window.semiautodata.procedures.map(p => ({ id: p.tag, text: p.value + " [" + p.tag + "]" }))
}); });
$select_procedure.val(null).trigger("change"); $select_procedure.val(null).trigger("change");
$select_procedure.on('change', function () { $select_procedure.on('change', function () {
@@ -814,10 +916,111 @@ function fill_items() {
update_all_previews(); update_all_previews();
}); });
$input_etad.on('input', function () {
update_all_previews();
});
} }
} }
function send_broadcast_message() {
console.log("send_broadcast_message");
if ($("#send_broadcast").hasClass("btn-disable")) {
alert("Cannot send broadcast message. Please complete all required fields.");
return;
}
// get all checked broadcast zones from tbody_broadcastzones
let selected_zones = [];
$tbody_broadcastzones.find("input[type='checkbox']").each(function () {
const $checkbox = $(this);
if ($checkbox.is(":checked")) {
selected_zones.push($checkbox.val());
}
});
if (selected_zones.length === 0) {
alert("Please select at least one broadcast zone.");
return;
}
let languages = [];
if ($enable_indonesia.is(":checked")) languages.push("INDONESIA");
if ($enable_english.is(":checked")) languages.push("ENGLISH");
if ($enable_chinese.is(":checked")) languages.push("CHINESE");
if ($enable_japanese.is(":checked")) languages.push("JAPANESE");
if ($enable_arabic.is(":checked")) languages.push("ARABIC");
if ($enable_local.is(":checked")) languages.push("LOCAL");
if (languages.length === 0) {
alert("Please select at least one language to send.");
return;
}
let description = selected_messages.length > 0 ? selected_messages[0].description : "";
if (!ValidString(description)) {
alert("Message not selected");
return;
}
let tags = [];
let msg = selected_messages[0];
tags.push("ANN_ID:"+msg.id);
if (msg.message_details.indexOf('[CITY]') !== -1) {
let cities = $select_city.val();
if (Array.isArray(cities) && cities.length > 0) {
tags.push("CITY:" + cities.join(";"));
}
}
if (msg.message_details.indexOf('[AIRPLANE_NAME]') !== -1) {
tags.push("AL:" + $select_airline.val());
}
if (msg.message_details.indexOf('[FLIGHT_NUMBER]') !== -1) {
tags.push("FLNUM:" + $input_flightnumber.val());
}
if (msg.message_details.indexOf('[PLATNOMOR]') !== -1) {
tags.push("PLATNOMOR:" + $input_licenseplate.val());
}
if (msg.message_details.indexOf('[PLACES]') !== -1) {
tags.push("PLACES:" + $select_places.val());
}
if (msg.message_details.indexOf('[ETAD]') !== -1) {
tags.push("ETAD:" + $input_etad.val());
}
if (msg.message_details.indexOf('[SHALAT]') !== -1) {
tags.push("SHALAT:" + $select_shalat.val());
}
if (msg.message_details.indexOf('[BCB]') !== -1) {
tags.push("BCB:" + $input_conveyorbelt.val());
}
if (msg.message_details.indexOf('[GATENUMBER]') !== -1) {
tags.push("GATECODE:" + $input_gatenumber.val());
}
if (msg.message_details.indexOf('[REASON]') !== -1) {
tags.push("REASON:" + $select_reason.val());
}
if (msg.message_details.indexOf('[PROCEDURE]') !== -1) {
tags.push("PROCEDURE:" + $select_procedure.val());
}
let payload = {
description: description,
languages: languages.join(";"),
tags: tags.join(" "),
broadcastzones: selected_zones.join(";")
}
console.log("Payload:", payload);
fetchAPI("SemiAuto", "POST", {}, payload, (data) => {
alert("Broadcast message sent successfully.");
}, (error) => {
console.error("Error:", error);
alert("Error sending broadcast message: " + error);
});
}
// App start here // App start here
$(document).ready(function () { $(document).ready(function () {
console.log("javascript loaded"); console.log("javascript loaded");
@@ -846,8 +1049,7 @@ $(document).ready(function () {
$input_flightnumber = $("#input_flightnumber"); $input_flightnumber = $("#input_flightnumber");
$input_licenseplate = $("#input_licenseplate"); $input_licenseplate = $("#input_licenseplate");
$input_conveyorbelt = $("#input_conveyorbelt"); $input_conveyorbelt = $("#input_conveyorbelt");
$input_hours = $("#input_hours"); $input_etad = $("#input_etad");
$input_minutes = $("#input_minutes");
$row_airplane = $("#row_airplane"); $row_airplane = $("#row_airplane");
$row_city = $("#row_city"); $row_city = $("#row_city");
$row_gatenumber = $("#row_gatenumber"); $row_gatenumber = $("#row_gatenumber");
@@ -864,5 +1066,9 @@ $(document).ready(function () {
reload_database(); reload_database();
}); });
$("#send_broadcast").off("click").on("click", function () {
send_broadcast_message();
});
}); });

View File

@@ -110,7 +110,7 @@
<div class="col-5 pad-row-input"> <div class="col-5 pad-row-input">
<div class="row"> <div class="row">
<div class="col-5"><label class="col-form-label">Flight Number</label></div> <div class="col-5"><label class="col-form-label">Flight Number</label></div>
<div class="col-7 pad-select"><input type="text" id="input_flightnumber" class="form-control pad-input"></div> <div class="col-7 pad-select"><input type="text" id="input_flightnumber" class="form-control pad-input" placeholder="flight number" minlength="1" maxlength="4" inputmode="numeric"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -123,18 +123,8 @@
<div class="col-8 col-sm-8 col-md-9 col-lg-9 col-xl-10 pad-input-left pad-row-input"><input type="text" id="input_gatenumber" class="pad-input form-control" placeholder="gate number"></div> <div class="col-8 col-sm-8 col-md-9 col-lg-9 col-xl-10 pad-input-left pad-row-input"><input type="text" id="input_gatenumber" class="pad-input form-control" placeholder="gate number"></div>
</div> </div>
<div class="row" id="row_time"> <div class="row" id="row_time">
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6 pad-row-input"> <div class="col-4 col-sm-4 col-md-3 col-lg-3 col-xl-2 pad-row-input"><label class="col-form-label">ETAD</label></div>
<div class="row"> <div class="col"><input id="input_etad" type="time"></div>
<div class="col-4 col-sm-4 col-md-6 col-lg-6 col-xl-4"><label class="col-form-label">Hours</label></div>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-8 pad-select"><input type="number" id="input_hours" class="form-control pad-input" value="0" min="0" max="23" step="1" placeholder="hour"></div>
</div>
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6 pad-row-input">
<div class="row">
<div class="col-4 col-sm-4 col-md-6 col-lg-6 col-xl-4"><label class="col-form-label">Minutes</label></div>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-8 pad-select"><input type="number" id="input_minutes" class="form-control pad-input" value="0" min="0" max="59" step="1" placeholder="minute"></div>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6 pad-row-input" id="col_reason"> <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6 pad-row-input" id="col_reason">
@@ -236,7 +226,7 @@
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path> <path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
<path d="M20 4v5h-5"></path> <path d="M20 4v5h-5"></path>
</svg>&nbsp;Reload Database</button></div> </svg>&nbsp;Reload Database</button></div>
<div class="col-sm-6 col-md-6 col-lg-6 col-xl-4 col-xxl-4 py-2"><button class="btn btn-primary w-100 pad-input btn-broadcast" id="send_broadcast" type="button" style="font-family: Raleway, sans-serif;box-shadow: 0px 0px 0px 0px var(--bs-blue);border-color: rgba(255,255,255,0.5);"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icon-tabler-send pad-icon"> <div class="col-sm-6 col-md-6 col-lg-6 col-xl-4 col-xxl-4 py-2"><button class="btn w-100 pad-input btn-disable" id="send_broadcast" type="button" style="font-family: Raleway, sans-serif;box-shadow: 0px 0px 0px 0px var(--bs-blue);border-color: rgba(255,255,255,0.5);"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icon-tabler-send pad-icon">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 14l11 -11"></path> <path d="M10 14l11 -11"></path>
<path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5"></path> <path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5"></path>

View File

@@ -1337,7 +1337,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="accordion-item invisible pad-accordion"> <div class="accordion-item pad-accordion">
<h2 class="accordion-header" role="tab"><button class="accordion-button bg-heading3" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-4" aria-expanded="true" aria-controls="accordion-1 .item-4">Remote Listening</button></h2> <h2 class="accordion-header" role="tab"><button class="accordion-button bg-heading3" type="button" data-bs-toggle="collapse" data-bs-target="#accordion-1 .item-4" aria-expanded="true" aria-controls="accordion-1 .item-4">Remote Listening</button></h2>
<div class="accordion-collapse collapse show item-4" role="tabpanel" data-bs-parent="#accordion-1"> <div class="accordion-collapse collapse show item-4" role="tabpanel" data-bs-parent="#accordion-1">
<div class="accordion-body"> <div class="accordion-body">

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -401,7 +401,7 @@ class MainExtension01 {
"[ETAD]" -> { "[ETAD]" -> {
val values = variables["ETAD"].orEmpty().split(":").map { it.trim() }.filter { IsNumber(it) } val values = variables["ETAD"].orEmpty().split(":").map { it.trim() }.filter { IsNumber(it) }
println("ETAD values: $values") //println("ETAD values: $values")
if (values.size == 2) { if (values.size == 2) {
if (IsNumber(values[0]) && IsNumber(values[1])) { if (IsNumber(values[0]) && IsNumber(values[1])) {
val _h = values[0].toInt() val _h = values[0].toInt()

View File

@@ -16,9 +16,12 @@ import contentCache
import org.tinylog.Logger import org.tinylog.Logger
@Suppress("unused") @Suppress("unused")
class AudioPlayer (var samplingrate: Int) { class AudioPlayer (var samplingrate: Int = 44100) {
val bass: Bass = Bass.Instance val bass: Bass = Bass.Instance
val bassenc : BassEnc = BassEnc.Instance val bassenc : BassEnc = BassEnc.Instance
val bassencmp3: BassEncMP3 = BassEncMP3.Instance
val bassencopus: BassEncOpus = BassEncOpus.Instance
val bassencogg : BassEncOGG = BassEncOGG.Instance
var initedDevice = -1 var initedDevice = -1
@@ -28,6 +31,9 @@ class AudioPlayer (var samplingrate: Int) {
if (samplingrate<1) samplingrate = 44100 // Default sampling rate if (samplingrate<1) samplingrate = 44100 // Default sampling rate
Logger.info {"Bass version ${Integer.toHexString(bass.BASS_GetVersion())}"} Logger.info {"Bass version ${Integer.toHexString(bass.BASS_GetVersion())}"}
Logger.info { "BassEnc version ${Integer.toHexString(bassenc.BASS_Encode_GetVersion())}" } Logger.info { "BassEnc version ${Integer.toHexString(bassenc.BASS_Encode_GetVersion())}" }
Logger.info { "BassEncMP3 version ${Integer.toHexString(bassencmp3.BASS_Encode_MP3_GetVersion())}" }
Logger.info { "BassEncOpus version ${Integer.toHexString(bassencopus.BASS_Encode_OPUS_GetVersion())}" }
Logger.info {" BassEncOGG version ${Integer.toHexString(bassencogg.BASS_Encode_OGG_GetVersion())}"}
InitAudio(0) // Audio 0 is No Sound, use for reading and writing wav silently InitAudio(0) // Audio 0 is No Sound, use for reading and writing wav silently
} }

View File

@@ -88,7 +88,7 @@ public interface BassEnc extends Library {
* @param length number of bytes * @param length number of bytes
* @param user the user pointer passed to BASS_Encode_Start * @param user the user pointer passed to BASS_Encode_Start
*/ */
void ENCODEPROC(int encoderhandle, int channelhandle, Memory encodedData, int length, Pointer user); void ENCODEPROC(int encoderhandle, int channelhandle, Pointer encodedData, int length, Pointer user);
} }
interface ENCODEPROCEX extends Callback { interface ENCODEPROCEX extends Callback {
@@ -101,7 +101,7 @@ public interface BassEnc extends Library {
* @param offset file offset of the data * @param offset file offset of the data
* @param user the user pointer passed to BASS_Encode_Start * @param user the user pointer passed to BASS_Encode_Start
*/ */
void ENCODEPROCEX(int handle, int channel, Memory buffer, int length, long offset, Object user); void ENCODEPROCEX(int handle, int channel, Pointer buffer, int length, long offset, Pointer user);
} }
interface ENCODERPROC extends Callback { interface ENCODERPROC extends Callback {
@@ -115,7 +115,7 @@ public interface BassEnc extends Library {
* @param user the user pointer passed to BASS_Encode_Start * @param user the user pointer passed to BASS_Encode_Start
* @return the amount of encoded data (-1 = stop) * @return the amount of encoded data (-1 = stop)
*/ */
int ENCODERPROC(int encoderHandle, int channelHandle, Memory encodedData, int length, int maxOut, Pointer user); int ENCODERPROC(int encoderHandle, int channelHandle, Pointer encodedData, int length, int maxOut, Pointer user);
} }

13
src/audio/BassEncMP3.java Normal file
View File

@@ -0,0 +1,13 @@
package audio;
import com.sun.jna.Library;
@SuppressWarnings("unused")
public interface BassEncMP3 extends Library {
BassEncMP3 Instance = (BassEncMP3) com.sun.jna.Native.load("bassenc_mp3", BassEncMP3.class);
int BASS_Encode_MP3_GetVersion();
int BASS_Encode_MP3_Start(int handle, String options, int flags, BassEnc.ENCODEPROCEX proc, Object user);
int BASS_Encode_MP3_StartFile(int handle, String options, int flags, String filename);
}

15
src/audio/BassEncOGG.java Normal file
View File

@@ -0,0 +1,15 @@
package audio;
import com.sun.jna.Library;
@SuppressWarnings("unused")
public interface BassEncOGG extends Library {
BassEncOGG Instance = (BassEncOGG) com.sun.jna.Native.load("bassenc_ogg", BassEncOGG.class);
int BASS_ENCODE_OGG_RESET = 0x1000000;
int BASS_Encode_OGG_GetVersion();
int BASS_Encode_OGG_Start(int handle, String options, int flags, BassEnc.ENCODEPROC proc, Object user);
int BASS_Encode_OGG_StartFile(int handle, String options, int flags, String filename);
boolean BASS_Encode_OGG_NewStream(int handle, String options, int flags);
}

View File

@@ -0,0 +1,18 @@
package audio;
import com.sun.jna.Library;
@SuppressWarnings("unused")
public interface BassEncOpus extends Library {
BassEncOpus Instance = (BassEncOpus) com.sun.jna.Native.load("bassenc_opus", BassEncOpus.class);
int BASS_ENCODE_OPUS_RESET = 0x1000000;
int BASS_ENCODE_OPUS_CTLONLY = 0x2000000;
int BASS_Encode_OPUS_GetVersion();
int BASS_Encode_OPUS_Start(int handle, String options, int flags, BassEnc.ENCODEPROC proc, Object user);
int BASS_Encode_OPUS_StartFile(int handle, String options, int flags, String filename);
boolean BASS_Encode_OPUS_NewStream(int handle, String options, int flags);
}

126
src/audio/Mp3Encoder.kt Normal file
View File

@@ -0,0 +1,126 @@
package audio
import audioPlayer
import com.sun.jna.Memory
import com.sun.jna.Pointer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.util.function.Consumer
@Suppress("unused")
class Mp3Encoder(val samplingrate: Int=44100, val channels: Int=1) {
var push_handle: Int = 0
var mp3_handle: Int = 0
private val bass = audioPlayer.bass
private val bassencmp3 = audioPlayer.bassencmp3
var callback : Consumer<ByteArray>? = null
/**
* Check if the encoder is started
* @return true if started, false otherwise
*/
fun isStarted() : Boolean {
return push_handle!=0 && mp3_handle!=0
}
val proc = BassEnc.ENCODEPROCEX{
enchandle, sourcehandle, buffer, length, offset, user ->
if (enchandle == mp3_handle) {
if (sourcehandle == push_handle) {
val data = ByteArray(length)
buffer.read(0, data, 0, length)
callback?.accept(data)
} else Logger.error { "MP3 Encoder callback called with unknown source handle: $sourcehandle" }
} else Logger.error { "MP3 Encoder callback called with unknown encoder handle: $enchandle" }
}
/**
* Start the MP3 encoder
* @param cb Function to receive encoded MP3 data
*/
fun Start(cb: Consumer<ByteArray>){
callback = null
push_handle = bass.BASS_StreamCreate(samplingrate, channels, Bass.BASS_STREAM_DECODE, Pointer(-1), null)
if (push_handle!=0){
Logger.info{"MP3 Encoder initialized with sampling rate $samplingrate Hz and $channels channel(s)" }
mp3_handle = bassencmp3.BASS_Encode_MP3_Start(push_handle, null, BassEnc.BASS_ENCODE_AUTOFREE,proc, null)
if (mp3_handle!=0){
callback = cb
CoroutineScope(Dispatchers.Default).launch {
val readsize = 4*1024 // 4 K buffer
Logger.info{"MP3 Encoder started successfully." }
while(isActive && push_handle!=0){
val p = Memory(readsize.toLong())
val read = bass.BASS_ChannelGetData(push_handle, p, readsize)
if (read==-1){
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to read data from MP3 Encoder stream. BASS error code: $err" }
break
}
delay(2)
}
Logger.info{"MP3 Encoder finished successfully." }
}
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to start MP3 Encoder. BASS error code: $err"}
}
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to initialize MP3 Encoder. BASS error code: $err" }
}
}
/**
* Push PCM data to be encoded
* @param data PCM data in ByteArray
* @return Number of bytes written, or 0 if failed
*/
fun PushData(data: ByteArray): Int {
if (push_handle==0){
Logger.error{"MP3 Encoder is not started." }
return 0
}
val mem = Memory(data.size.toLong())
mem.write(0, data, 0, data.size)
val written = bass.BASS_StreamPutData(push_handle, mem, data.size)
if (written==-1){
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to push data to MP3 Encoder. BASS error code: $err" }
return 0
}
//println("MP3 Encoder: Pushed $written bytes of PCM data.")
return written
}
/**
* Stop the MP3 encoder and free resources
*/
fun Stop(){
if (push_handle!=0){
val res = bass.BASS_StreamFree(push_handle)
if (res){
Logger.info{"MP3 Encoder stream freed successfully." }
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to free MP3 Encoder stream. BASS error code: $err"}
}
push_handle = 0
}
// auto close by BASS_ENCODE_AUTOFREE
mp3_handle = 0
callback = null
println("MP3 Encoder: Stopped.")
}
}

127
src/audio/OpusEncoder.kt Normal file
View File

@@ -0,0 +1,127 @@
package audio
import audioPlayer
import com.sun.jna.Memory
import com.sun.jna.Pointer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.tinylog.Logger
import java.util.function.Consumer
@Suppress("unused")
class OpusEncoder(val samplingrate: Int=44100, val channels: Int=1) {
var push_handle: Int = 0
var opus_handle: Int = 0
private val bass = audioPlayer.bass
private val bassencopus = audioPlayer.bassencopus
var callback : Consumer<ByteArray>? = null
/**
* Check if the encoder is started
* @return true if started, false otherwise
*/
fun isStarted() : Boolean {
return push_handle!=0 && opus_handle!=0
}
val proc = BassEnc.ENCODEPROC{
enchandle, sourcehandle, buffer, length, user ->
if (enchandle == opus_handle) {
if (sourcehandle == push_handle) {
val data = ByteArray(length)
buffer.read(0, data, 0, length)
callback?.accept(data)
//println("MP3 Encoder callback: Sent $length bytes")
} else Logger.error { "Opus Encoder callback called with unknown source handle: $sourcehandle" }
} else Logger.error { "Opus Encoder callback called with unknown encoder handle: $enchandle" }
}
/**
* Start the Opus encoder
* @param cb Function to receive encoded MP3 data
*/
fun Start(cb: Consumer<ByteArray>){
callback = null
push_handle = bass.BASS_StreamCreate(samplingrate, channels, Bass.BASS_STREAM_DECODE, Pointer(-1), null)
if (push_handle!=0){
Logger.info{"Opus Encoder initialized with sampling rate $samplingrate Hz and $channels channel(s)" }
opus_handle = bassencopus.BASS_Encode_OPUS_Start(push_handle, null, BassEnc.BASS_ENCODE_AUTOFREE, proc, null)
if (opus_handle!=0){
callback = cb
CoroutineScope(Dispatchers.Default).launch {
val readsize = 8 * 1024 * 1024 // 8 MB buffer
Logger.info{"Opus Encoder started successfully." }
while(isActive && push_handle!=0){
delay(2)
val p = Memory(readsize.toLong())
val read = bass.BASS_ChannelGetData(push_handle, p, readsize)
if (read==-1){
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to read data from Opus Encoder stream. BASS error code: $err" }
break
}
}
Logger.info{"Opus Encoder finished successfully." }
}
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to start Opus Encoder. BASS error code: $err"}
}
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to initialize Opus Encoder. BASS error code: $err" }
}
}
/**
* Push PCM data to be encoded
* @param data PCM data in ByteArray
* @return Number of bytes written, or 0 if failed
*/
fun PushData(data: ByteArray): Int {
if (push_handle==0){
Logger.error{"Opus Encoder is not started." }
return 0
}
val mem = Memory(data.size.toLong())
mem.write(0, data, 0, data.size)
val written = bass.BASS_StreamPutData(push_handle, mem, data.size)
if (written==-1){
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to push data to Opus Encoder. BASS error code: $err" }
return 0
}
//println("Opus Encoder: Pushed $written bytes of PCM data.")
return written
}
/**
* Stop the Opus encoder and free resources
*/
fun Stop(){
if (push_handle!=0){
val res = bass.BASS_StreamFree(push_handle)
if (res){
Logger.info{"Opus Encoder stream freed successfully." }
} else {
val err = bass.BASS_ErrorGetCode()
Logger.error{"Failed to free Opus Encoder stream. BASS error code: $err"}
}
push_handle = 0
}
// auto close by BASS_ENCODE_AUTOFREE
opus_handle = 0
callback = null
println("Opus Encoder: Stopped.")
}
}

View File

@@ -1,5 +1,7 @@
package barix package barix
import audio.Mp3Encoder
import audio.OpusEncoder
import codes.Somecodes import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -24,37 +26,37 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private val inet = InetSocketAddress(ipaddress, port) private val inet = InetSocketAddress(ipaddress, port)
private val maxUDPsize = 1000 private val maxUDPsize = 1000
private var _tcp: Socket? = null private var _tcp: Socket? = null
private val PipeOuts = mutableMapOf<String,PipedOutputStream>() private val pipeOuts = mutableMapOf<String,PipedOutputStream>()
private val mp3encoder = Mp3Encoder()
fun AddPipeOut(key: String, pipeOut: PipedOutputStream) { fun AddPipeOut(key: String, pipeOut: PipedOutputStream) {
RemovePipeOut(key) RemovePipeOut(key)
PipeOuts[key] = pipeOut pipeOuts[key] = pipeOut
println("Added pipeOut $key to BarixConnection $channel ($ipaddress)") println("Added pipeOut $key to BarixConnection $channel ($ipaddress)")
} }
fun RemovePipeOut(key: String) { fun RemovePipeOut(key: String) {
println("Removing pipeOut $key") if (pipeOuts.contains(key)){
if (PipeOuts.contains(key)){ val pipe = pipeOuts[key]
val pipe = PipeOuts[key]
try { try {
pipe?.close() pipe?.close()
} catch (e: Exception) { } catch (e: Exception) {
// ignore // ignore
} }
println("Removed pipeOut $key from BarixConnection $channel ($ipaddress)") println("Removed pipeOut $key from BarixConnection $channel ($ipaddress)")
PipeOuts.remove(key) pipeOuts.remove(key)
} }
} }
fun ClearPipeOuts() { fun ClearPipeOuts() {
PipeOuts.values.forEach { piped -> pipeOuts.values.forEach { piped ->
try { try {
piped.close() piped.close()
} catch (e: Exception) { } catch (e: Exception) {
// ignore // ignore
} }
} }
PipeOuts.clear() pipeOuts.clear()
} }
/** /**
* Buffer remain in bytes * Buffer remain in bytes
@@ -137,34 +139,40 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
DatagramSocket().use{ udp -> DatagramSocket().use{ udp ->
val bb = ByteBuffer.wrap(data) val bb = ByteBuffer.wrap(data)
if (!mp3encoder.isStarted()) {
mp3encoder.Start { data ->
pipeOuts.keys.forEach { kk ->
val pp = pipeOuts[kk]
try {
pp?.write(data)
pp?.flush()
println("Written ${data.size} bytes to pipeOut $kk")
} catch (e: Exception) {
Logger.error { "Failed to write to pipeOut $kk, message: ${e.message}" }
pp?.close()
pipeOuts.remove(kk)
}
}
}
}
while(bb.hasRemaining()){ while(bb.hasRemaining()){
try { try {
val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining()) val chunk = ByteArray(if (bb.remaining() > maxUDPsize) maxUDPsize else bb.remaining())
bb.get(chunk) bb.get(chunk)
//println("Buffer remain: $bufferRemain, sending chunk size: ${chunk.size}")
while(bufferRemain<chunk.size){ while(bufferRemain<chunk.size){
delay(10) delay(10)
} }
udp.send(DatagramPacket(chunk, chunk.size, inet)) udp.send(DatagramPacket(chunk, chunk.size, inet))
mp3encoder.PushData(chunk)
delay(2) delay(2)
PipeOuts.keys.forEach { kk ->
val pp = PipeOuts[kk]
try {
pp?.write(chunk)
pp?.flush()
println("Written ${chunk.size} bytes to pipeOut $kk")
} catch (e: Exception) {
Logger.error { "Failed to write to pipeOut $kk, message: ${e.message}" }
pp?.close()
PipeOuts.remove(kk)
}
}
} catch (e: Exception) { } catch (e: Exception) {
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}") cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
return@launch return@launch
} }
} }
mp3encoder.Stop()
cbOK.accept("SendData to $channel ($ipaddress) succeeded, ${data.size} bytes sent") cbOK.accept("SendData to $channel ($ipaddress) succeeded, ${data.size} bytes sent")
} }
} }

View File

@@ -49,13 +49,14 @@ import java.io.File
import java.io.PipedInputStream import java.io.PipedInputStream
import java.io.PipedOutputStream import java.io.PipedOutputStream
import java.nio.file.Path import java.nio.file.Path
import java.util.UUID
@Suppress("unused") @Suppress("unused")
class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val _config: configFile) { class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val _config: configFile) {
lateinit var app: Javalin lateinit var app: Javalin
lateinit var semiauto : Javalin lateinit var semiauto: Javalin
val objectmapper = jacksonObjectMapper() val objectmapper = jacksonObjectMapper()
private fun SendReply(context: WsMessageContext, command: String, value: String) { private fun SendReply(context: WsMessageContext, command: String, value: String) {
@@ -73,7 +74,7 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
} }
private fun Start_WebServer(){ private fun Start_WebServer() {
Logger.info { "Starting Web Application on port $listenPort" } Logger.info { "Starting Web Application on port $listenPort" }
app = Javalin.create { config -> app = Javalin.create { config ->
config.useVirtualThreads = true config.useVirtualThreads = true
@@ -253,15 +254,26 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
if (param.isNotEmpty()) { if (param.isNotEmpty()) {
val bc = Get_Barix_Connection_by_ZoneName(param) val bc = Get_Barix_Connection_by_ZoneName(param)
if (bc != null) { if (bc != null) {
val key = ctx.req().remoteAddr + ":" + ctx.req().remotePort
val key = ctx.cookie("client-id") ?: UUID.randomUUID().toString()
.also { ctx.cookie("client-id", it) }
val pipeIN = PipedInputStream(8192) val pipeIN = PipedInputStream(8192)
val pipeOUT = PipedOutputStream(pipeIN) val pipeOUT = PipedOutputStream(pipeIN)
// write WAV Header 44 bytes contains samplingrate 44100 Hz, 16 bit, mono // write WAV Header 44 bytes contains samplingrate 44100 Hz, 16 bit, mono
pipeOUT.write(Generate_WAV_Header()) // pipeOUT.write(Generate_WAV_Header())
bc.AddPipeOut(key, pipeOUT) bc.AddPipeOut(key, pipeOUT)
ctx.contentType("audio/wav") ctx.contentType("audio/mpeg")
ctx.header("Cache-Control", "no-cache") ctx.header("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.header("Pragma", "no-cache")
ctx.header("Expires", "0")
// 🔥 This one is critical — tells Jetty to send data in HTTP chunks as it becomes available:
ctx.header("Transfer-Encoding", "chunked")
// Keeps the TCP socket open so the browser doesnt close after initial data
ctx.header("Connection", "keep-alive") ctx.header("Connection", "keep-alive")
ctx.result(pipeIN) ctx.result(pipeIN)
println("LiveAudio Open for zone $param SUCCESS") println("LiveAudio Open for zone $param SUCCESS")
} else ctx.status(400) } else ctx.status(400)
@@ -273,17 +285,23 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
get("Close/{broadcastzone}") { ctx -> get("Close/{broadcastzone}") { ctx ->
val param = ctx.pathParam("broadcastzone") val param = ctx.pathParam("broadcastzone")
println("LiveAudio Close for zone $param") println("LiveAudio Close for zone $param")
if (param.isNotEmpty()) { val key = ctx.cookie("client-id")
val bc = Get_Barix_Connection_by_ZoneName(param) if (key != null && key.isNotEmpty()) {
if (bc != null) { if (param.isNotEmpty()) {
val key = ctx.req().remoteAddr + ":" + ctx.req().remotePort val bc = Get_Barix_Connection_by_ZoneName(param)
bc.RemovePipeOut(key) if (bc != null) {
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
println("LiveAudio Close for zone $param SUCCESS") bc.RemovePipeOut(key)
ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
println("LiveAudio Close for zone $param SUCCESS")
} else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found")))
} else ctx.status(400) } else ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("Broadcastzone not found"))) .result(objectmapper.writeValueAsString(resultMessage("Invalid broadcastzone")))
} else ctx.status(400) } else {
.result(objectmapper.writeValueAsString(resultMessage("Invalid broadcastzone"))) ctx.status(400)
.result(objectmapper.writeValueAsString(resultMessage("No client-id cookie found")))
}
} }
} }
path("VoiceType") { path("VoiceType") {
@@ -2043,55 +2061,55 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
}.start(listenPort) }.start(listenPort)
} }
private fun Start_SemiAutoServer(){ private fun Start_SemiAutoServer() {
Logger.info {"Starting SemiAuto web server at port ${listenPort+1}"} Logger.info { "Starting SemiAuto web server at port ${listenPort + 1}" }
semiauto = Javalin.create{ config -> semiauto = Javalin.create { config ->
config.useVirtualThreads = true config.useVirtualThreads = true
config.staticFiles.add ("/semiauto") config.staticFiles.add("/semiauto")
config.jsonMapper(JavalinJackson(jacksonObjectMapper())) config.jsonMapper(JavalinJackson(jacksonObjectMapper()))
config.router.apiBuilder { config.router.apiBuilder {
path("/"){ path("/") {
get { get {
it.cookie("semiauto-user","") it.cookie("semiauto-user", "")
it.redirect("login.html") it.redirect("login.html")
} }
} }
path("logout"){ path("logout") {
get { get {
it.cookie("semiauto-user","") it.cookie("semiauto-user", "")
it.redirect("login.html") it.redirect("login.html")
} }
} }
path("login.html"){ path("login.html") {
post { post {
val formuser = it.formParam("username") ?: "" val formuser = it.formParam("username") ?: ""
val formpass = it.formParam("password") ?: "" val formpass = it.formParam("password") ?: ""
if (formuser.isNotEmpty() && formpass.isNotEmpty()){ if (formuser.isNotEmpty() && formpass.isNotEmpty()) {
val user = db.userDB.List.find { u -> u.username==formuser && u.password==formpass } val user = db.userDB.List.find { u -> u.username == formuser && u.password == formpass }
if (user!=null){ if (user != null) {
it.cookie("semiauto-user",user.username) it.cookie("semiauto-user", user.username)
it.redirect("index.html") it.redirect("index.html")
} else ResultMessageString(it, 400, "Invalid username or password") } else ResultMessageString(it, 400, "Invalid username or password")
} else ResultMessageString(it, 400, "Username or password cannot be empty") } else ResultMessageString(it, 400, "Username or password cannot be empty")
} }
} }
path("index.html"){ path("index.html") {
before { CheckSemiAutoUsers(it) } before { CheckSemiAutoUsers(it) }
} }
path("log.html"){ path("log.html") {
before { CheckSemiAutoUsers(it) } before { CheckSemiAutoUsers(it) }
} }
path("api"){ path("api") {
path("Initialize"){ path("Initialize") {
get { ctx -> get { ctx ->
val username = ctx.cookie("semiauto-user") ?: "" val username = ctx.cookie("semiauto-user") ?: ""
if (username.isNotEmpty()){ if (username.isNotEmpty()) {
val user = db.userDB.List.find { u -> u.username==username } val user = db.userDB.List.find { u -> u.username == username }
if (user!=null){ if (user != null) {
val result = SemiAutoInitData(username) val result = SemiAutoInitData(username)
// messages // messages
String_To_List(user.messagebank_ann_id).forEach { msg -> String_To_List(user.messagebank_ann_id).forEach { msg ->
db.messageDB.List.filter{it.ANN_ID==msg.toUInt()}.forEach {xx -> db.messageDB.List.filter { it.ANN_ID == msg.toUInt() }.forEach { xx ->
result.messages.add("${xx.ANN_ID};${xx.Description};${xx.Language};${xx.Message_Detail}") result.messages.add("${xx.ANN_ID};${xx.Description};${xx.Language};${xx.Message_Detail}")
} }
} }
@@ -2100,47 +2118,55 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
// cities // cities
String_To_List(user.city_tags).forEach { ct -> String_To_List(user.city_tags).forEach { ct ->
db.soundDB.List.firstOrNull { it.TAG == ct && it.Category == Category.City.name } db.soundDB.List.firstOrNull { it.TAG == ct && it.Category == Category.City.name }
?.let{ ?.let {
result.cities.add("${it.TAG};${it.Description}") result.cities.add("${it.TAG};${it.Description}")
} }
} }
// airplane names // airplane names
String_To_List(user.airline_tags).forEach { at -> String_To_List(user.airline_tags).forEach { at ->
db.soundDB.List.firstOrNull { it.TAG == at && it.Category == Category.Airplane_Name.name } db.soundDB.List.firstOrNull { it.TAG == at && it.Category == Category.Airplane_Name.name }
?.let{ ?.let {
result.airlines.add("${it.TAG};${it.Description}") result.airlines.add("${it.TAG};${it.Description}")
} }
} }
// places // places
db.soundDB.List.filter{it.Category==Category.Places.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Places.name }.distinctBy { it.TAG }
result.places.add("${xx.TAG};${xx.Description}") .forEach { xx ->
} result.places.add("${xx.TAG};${xx.Description}")
}
// shalat // shalat
db.soundDB.List.filter{it.Category==Category.Shalat.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Shalat.name }.distinctBy { it.TAG }
result.shalat.add("${xx.TAG};${xx.Description}") .forEach { xx ->
} result.shalat.add("${xx.TAG};${xx.Description}")
}
// sequences // sequences
db.soundDB.List.filter{it.Category==Category.Sequence.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Sequence.name }
.distinctBy { it.TAG }.forEach { xx ->
result.sequences.add("${xx.TAG};${xx.Description}") result.sequences.add("${xx.TAG};${xx.Description}")
} }
// reasons // reasons
db.soundDB.List.filter{it.Category==Category.Reason.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Reason.name }.distinctBy { it.TAG }
result.reasons.add("${xx.TAG};${xx.Description}") .forEach { xx ->
} result.reasons.add("${xx.TAG};${xx.Description}")
}
// procedures // procedures
db.soundDB.List.filter{it.Category==Category.Procedure.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Procedure.name }
.distinctBy { it.TAG }.forEach { xx ->
result.procedures.add("${xx.TAG};${xx.Description}") result.procedures.add("${xx.TAG};${xx.Description}")
} }
// gates // gates
db.soundDB.List.filter{it.Category==Category.Gate.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Gate.name }.distinctBy { it.TAG }
result.gates.add("${xx.TAG};${xx.Description}") .forEach { xx ->
} result.gates.add("${xx.TAG};${xx.Description}")
}
// compensation // compensation
db.soundDB.List.filter{it.Category==Category.Compensation.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Compensation.name }
.distinctBy { it.TAG }.forEach { xx ->
result.compensation.add("${xx.TAG};${xx.Description}") result.compensation.add("${xx.TAG};${xx.Description}")
} }
// greetings // greetings
db.soundDB.List.filter{it.Category==Category.Greeting.name}.distinctBy { it.TAG }.forEach { xx -> db.soundDB.List.filter { it.Category == Category.Greeting.name }
.distinctBy { it.TAG }.forEach { xx ->
result.greetings.add("${xx.TAG};${xx.Description}") result.greetings.add("${xx.TAG};${xx.Description}")
} }
@@ -2149,18 +2175,18 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
} else ResultMessageString(ctx, 400, "Username is empty") } else ResultMessageString(ctx, 400, "Username is empty")
} }
} }
path("SemiAuto"){ path("SemiAuto") {
post { ctx -> post { ctx ->
val json : JsonNode = objectmapper.readTree(ctx.body()) val json: JsonNode = objectmapper.readTree(ctx.body())
// butuh description, languages, tags, dan broadcastzones // butuh description, languages, tags, dan broadcastzones
val description = json.get("description").asText("") val description = json.get("description").asText("")
val languages = json.get("languages").asText("") val languages = json.get("languages").asText("")
val tags = json.get("tags").asText("") val tags = json.get("tags").asText("")
val broadcastzones = json.get("broadcastzones").asText("") val broadcastzones = json.get("broadcastzones").asText("")
if (description.isNotEmpty()){ if (description.isNotEmpty()) {
if (languages.isNotEmpty()){ if (languages.isNotEmpty()) {
if (tags.isNotEmpty()){ if (tags.isNotEmpty()) {
if (broadcastzones.isNotEmpty()){ if (broadcastzones.isNotEmpty()) {
val qt = QueueTable( val qt = QueueTable(
0u, 0u,
LocalDateTime.now().format(datetimeformat1), LocalDateTime.now().format(datetimeformat1),
@@ -2172,10 +2198,10 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
1u, 1u,
languages languages
) )
if (db.queuetableDB.Add(qt)){ if (db.queuetableDB.Add(qt)) {
db.queuetableDB.Resort() db.queuetableDB.Resort()
Logger.info{"SemiAutoWeb added to queue table: $qt" } Logger.info { "SemiAutoWeb added to queue table: $qt" }
println("SemiAuto added to queue table: ${objectmapper.writeValueAsString(qt)}") //println("SemiAuto added to queue table: ${objectmapper.writeValueAsString(qt)}")
ctx.result(objectmapper.writeValueAsString(resultMessage("OK"))) ctx.result(objectmapper.writeValueAsString(resultMessage("OK")))
} else ResultMessageString(ctx, 500, "Failed to add to queue table") } else ResultMessageString(ctx, 500, "Failed to add to queue table")
} else ResultMessageString(ctx, 400, "Broadcast zones cannot be empty") } else ResultMessageString(ctx, 400, "Broadcast zones cannot be empty")
@@ -2185,13 +2211,12 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
} }
} }
path("Log"){ path("Log") {
get("/{datelog}"){ ctx -> get("/{datelog}") { ctx ->
val datelog = ctx.pathParam("datelog") val datelog = ctx.pathParam("datelog")
if (ValidDate(datelog)){ if (ValidDate(datelog)) {
println("SemiAuto Get Log for date $datelog") println("SemiAuto Get Log for date $datelog")
db.GetLogForHtml(datelog){ db.GetLogForHtml(datelog) { loghtml ->
loghtml ->
val resultstring = objectmapper.writeValueAsString(loghtml) val resultstring = objectmapper.writeValueAsString(loghtml)
println("Log HTML for date $datelog: $resultstring") println("Log HTML for date $datelog: $resultstring")
ctx.result(resultstring) ctx.result(resultstring)
@@ -2202,18 +2227,19 @@ class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>, val
} }
} }
} }
}.start(listenPort+1) }.start(listenPort + 1)
} }
fun ResultMessageString(ctx: Context, code: Int, message: String) { fun ResultMessageString(ctx: Context, code: Int, message: String) {
ctx.status(code).result(objectmapper.writeValueAsString(resultMessage(message))) ctx.status(code).result(objectmapper.writeValueAsString(resultMessage(message)))
} }
fun CheckSemiAutoUsers(ctx: Context){ fun CheckSemiAutoUsers(ctx: Context) {
val user = ctx.cookie("semiauto-user") val user = ctx.cookie("semiauto-user")
if (user == null) { if (user == null) {
ctx.redirect("login.html") ctx.redirect("login.html")
} }
val foundUser = db.userDB.List.find { u -> u.username==user } val foundUser = db.userDB.List.find { u -> u.username == user }
if (foundUser == null) { if (foundUser == null) {
ctx.redirect("login.html") ctx.redirect("login.html")
} }