update config

This commit is contained in:
jb-alvarado 2024-06-11 23:03:04 +02:00
parent 0dbdd1f46e
commit 39c51e9f80
9 changed files with 286 additions and 42 deletions

@ -1 +1 @@
Subproject commit 8d63cc4f85f3cbd530d509d74494b6fefbb9bf2c
Subproject commit 6225a2725465e5ff0d572b368346047e16d1be72

View File

@ -8,7 +8,11 @@
///
/// For all endpoints an (Bearer) authentication is required.\
/// `{id}` represent the channel id, and at default is 1.
use std::{env, path::PathBuf, sync::Mutex};
use std::{
env,
path::PathBuf,
sync::{atomic::Ordering, Mutex},
};
use actix_files;
use actix_multipart::Multipart;
@ -34,13 +38,15 @@ use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
use tokio::fs;
use crate::api::auth::{create_jwt, Claims};
use crate::player::utils::{
get_data_map, get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist,
};
use crate::utils::{
channels::{create_channel, delete_channel},
config::{PlayoutConfig, Template},
config::{
string_to_log_level, string_to_output_mode, string_to_processing_mode, PlayoutConfig,
Template,
},
control::{control_state, send_message, ControlParams, Process, ProcessCtl},
errors::ServiceError,
files::{
@ -52,6 +58,10 @@ use crate::utils::{
public_path, read_log_file, system, Role, TextFilter,
};
use crate::vec_strings;
use crate::{
api::auth::{create_jwt, Claims},
db::models::Configuration,
};
use crate::{
db::{
handles,
@ -493,7 +503,6 @@ async fn get_playout_config(
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
// let config = PlayoutConfig::new(&pool.into_inner(), *id).await;
Ok(web::Json(config))
}
@ -509,15 +518,80 @@ async fn get_playout_config(
async fn update_playout_config(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
_data: web::Json<PlayoutConfig>,
data: web::Json<PlayoutConfig>,
controllers: web::Data<Mutex<ChannelController>>,
) -> Result<impl Responder, ServiceError> {
if let Ok(_channel) = handles::select_channel(&pool.into_inner(), &id).await {
// TODO: update config
let manager = controllers.lock().unwrap().get(*id).unwrap();
let mut config = manager.config.lock().unwrap();
let id = config.general.id;
let channel_id = config.general.channel_id;
let db_config = Configuration::from(id, channel_id, data.into_inner());
return Ok("Update playout config success.");
};
if let Err(e) = handles::update_configuration(&pool.into_inner(), db_config.clone()).await {
return Err(ServiceError::Conflict(format!("{e}")));
}
Err(ServiceError::InternalServerError)
config.general.stop_threshold = db_config.stop_threshold;
config.mail.subject = db_config.subject;
config.mail.smtp_server = db_config.smtp_server;
config.mail.starttls = db_config.starttls;
config.mail.sender_addr = db_config.sender_addr;
config.mail.sender_pass = db_config.sender_pass;
config.mail.recipient = db_config.recipient;
config.mail.mail_level = string_to_log_level(db_config.mail_level);
config.mail.interval = db_config.interval as u64;
config.logging.ffmpeg_level = db_config.ffmpeg_level;
config.logging.ingest_level = db_config.ingest_level;
config.logging.detect_silence = db_config.detect_silence;
config.logging.ignore_lines = db_config
.ignore_lines
.split(";")
.map(|l| l.to_string())
.collect();
config.processing.mode = string_to_processing_mode(db_config.processing_mode);
config.processing.audio_only = db_config.audio_only;
config.processing.audio_track_index = db_config.audio_track_index;
config.processing.copy_audio = db_config.copy_audio;
config.processing.copy_video = db_config.copy_video;
config.processing.width = db_config.width;
config.processing.height = db_config.height;
config.processing.aspect = db_config.aspect;
config.processing.fps = db_config.fps;
config.processing.add_logo = db_config.add_logo;
config.processing.logo = db_config.logo;
config.processing.logo_scale = db_config.logo_scale;
config.processing.logo_opacity = db_config.logo_opacity;
config.processing.logo_position = db_config.logo_position;
config.processing.audio_tracks = db_config.audio_tracks;
config.processing.audio_channels = db_config.audio_channels;
config.processing.volume = db_config.volume;
config.processing.custom_filter = db_config.decoder_filter;
config.ingest.enable = db_config.ingest_enable;
config.ingest.input_param = db_config.ingest_param;
config.ingest.custom_filter = db_config.ingest_filter;
config.playlist.path = PathBuf::from(db_config.playlist_path);
config.playlist.day_start = db_config.day_start;
config.playlist.length = db_config.length;
config.playlist.infinit = db_config.infinit;
config.storage.path = PathBuf::from(db_config.storage_path);
config.storage.filler = PathBuf::from(db_config.filler);
config.storage.extensions = db_config
.extensions
.split(";")
.map(|l| l.to_string())
.collect();
config.storage.shuffle = db_config.shuffle;
config.text.add_text = db_config.add_text;
config.text.fontfile = db_config.fontfile;
config.text.text_from_filename = db_config.text_from_filename;
config.text.style = db_config.style;
config.text.regex = db_config.regex;
config.task.enable = db_config.task_enable;
config.task.path = PathBuf::from(db_config.task_path);
config.output.mode = string_to_output_mode(db_config.output_mode);
config.output.output_param = db_config.output_param;
Ok(web::Json("Update success"))
}
/// #### Text Presets
@ -727,6 +801,13 @@ pub async fn process_control(
let manager = controllers.lock().unwrap().get(*id).unwrap();
match proc.into_inner().command {
ProcessCtl::Status => {
if manager.is_alive.load(Ordering::SeqCst) {
return Ok(web::Json("active"));
} else {
return Ok(web::Json("not running"));
}
}
ProcessCtl::Start => {
manager.async_start().await;
}
@ -740,7 +821,7 @@ pub async fn process_control(
}
}
Ok(web::Json("no implemented"))
Ok(web::Json("Success"))
}
/// #### ffplayout Playlist Operations

View File

@ -147,6 +147,67 @@ pub async fn select_configuration(
sqlx::query_as(query).bind(channel).fetch_one(conn).await
}
pub async fn update_configuration(
conn: &Pool<Sqlite>,
config: Configuration,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "UPDATE configurations SET stop_threshold = $2, subject = $3, smtp_server = $4, sender_addr = $5, sender_pass = $6, recipient = $7, starttls = $8, mail_level = $9, interval = $10, ffmpeg_level = $11, ingest_level = $12, detect_silence = $13 , ignore_lines = $14, processing_mode = $15, audio_only = $16, copy_audio = $17, copy_video = $18, width = $19, height = $20, aspect = $21, add_logo = $22, logo = $23, logo_scale = $24, logo_opacity = $25, logo_position = $26, audio_tracks = $27, audio_track_index = $28, audio_channels = $29, volume = $30, decoder_filter = $31, ingest_enable = $32, ingest_param = $33, ingest_filter = $34, playlist_path = $35, day_start = $36, length = $37, infinit = $38, storage_path = $39, filler = $40, extensions = $41, shuffle = $42, add_text = $43, text_from_filename = $44, fontfile = $45, regex = $46, task_enable = $47, task_path = $48, output_mode = $49, output_param = $50 WHERE id = $1";
sqlx::query(query)
.bind(config.id)
.bind(config.stop_threshold)
.bind(config.subject)
.bind(config.smtp_server)
.bind(config.sender_addr)
.bind(config.sender_pass)
.bind(config.recipient)
.bind(config.starttls)
.bind(config.mail_level)
.bind(config.interval)
.bind(config.ffmpeg_level)
.bind(config.ingest_level)
.bind(config.detect_silence)
.bind(config.ignore_lines)
.bind(config.processing_mode)
.bind(config.audio_only)
.bind(config.copy_audio)
.bind(config.copy_video)
.bind(config.width)
.bind(config.height)
.bind(config.aspect)
.bind(config.add_logo)
.bind(config.logo)
.bind(config.logo_scale)
.bind(config.logo_opacity)
.bind(config.logo_position)
.bind(config.audio_tracks)
.bind(config.audio_track_index)
.bind(config.audio_channels)
.bind(config.volume)
.bind(config.decoder_filter)
.bind(config.ingest_enable)
.bind(config.ingest_param)
.bind(config.ingest_filter)
.bind(config.playlist_path)
.bind(config.day_start)
.bind(config.length)
.bind(config.infinit)
.bind(config.storage_path)
.bind(config.filler)
.bind(config.extensions)
.bind(config.shuffle)
.bind(config.add_text)
.bind(config.text_from_filename)
.bind(config.fontfile)
.bind(config.regex)
.bind(config.task_enable)
.bind(config.task_path)
.bind(config.output_mode)
.bind(config.output_param)
.execute(conn)
.await
}
pub async fn select_advanced_configuration(
conn: &Pool<Sqlite>,
channel: i32,

View File

@ -4,6 +4,8 @@ use serde::{
Deserialize, Serialize,
};
use crate::utils::config::PlayoutConfig;
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct User {
#[sqlx(default)]
@ -184,7 +186,6 @@ pub struct Configuration {
pub storage_help: String,
pub storage_path: String,
#[serde(alias = "filler_clip")]
pub filler: String,
pub extensions: String,
@ -206,6 +207,76 @@ pub struct Configuration {
pub output_param: String,
}
impl Configuration {
pub fn from(id: i32, channel_id: i32, config: PlayoutConfig) -> Self {
Self {
id,
channel_id,
general_help: config.general.help_text,
stop_threshold: config.general.stop_threshold,
mail_help: config.mail.help_text,
subject: config.mail.subject,
smtp_server: config.mail.smtp_server,
starttls: config.mail.starttls,
sender_addr: config.mail.sender_addr,
sender_pass: config.mail.sender_pass,
recipient: config.mail.recipient,
mail_level: config.mail.mail_level.to_string(),
interval: config.mail.interval as i64,
logging_help: config.logging.help_text,
ffmpeg_level: config.logging.ffmpeg_level,
ingest_level: config.logging.ingest_level,
detect_silence: config.logging.detect_silence,
ignore_lines: config.logging.ignore_lines.join(";"),
processing_help: config.processing.help_text,
processing_mode: config.processing.mode.to_string(),
audio_only: config.processing.audio_only,
audio_track_index: config.processing.audio_track_index,
copy_audio: config.processing.copy_audio,
copy_video: config.processing.copy_video,
width: config.processing.width,
height: config.processing.height,
aspect: config.processing.aspect,
fps: config.processing.fps,
add_logo: config.processing.add_logo,
logo: config.processing.logo,
logo_scale: config.processing.logo_scale,
logo_opacity: config.processing.logo_opacity,
logo_position: config.processing.logo_position,
audio_tracks: config.processing.audio_tracks,
audio_channels: config.processing.audio_channels,
volume: config.processing.volume,
decoder_filter: config.processing.custom_filter,
ingest_help: config.ingest.help_text,
ingest_enable: config.ingest.enable,
ingest_param: config.ingest.input_param,
ingest_filter: config.ingest.custom_filter,
playlist_help: config.playlist.help_text,
playlist_path: config.playlist.path.to_string_lossy().to_string(),
day_start: config.playlist.day_start,
length: config.playlist.length,
infinit: config.playlist.infinit,
storage_help: config.storage.help_text,
storage_path: config.storage.path.to_string_lossy().to_string(),
filler: config.storage.filler.to_string_lossy().to_string(),
extensions: config.storage.extensions.join(";"),
shuffle: config.storage.shuffle,
text_help: config.text.help_text,
add_text: config.text.add_text,
fontfile: config.text.fontfile,
text_from_filename: config.text.text_from_filename,
style: config.text.style,
regex: config.text.regex,
task_help: config.task.help_text,
task_enable: config.task.enable,
task_path: config.task.path.to_string_lossy().to_string(),
output_help: config.output.help_text,
output_mode: config.output.mode.to_string(),
output_param: config.output.output_param,
}
}
}
fn default_track_index() -> i32 {
-1
}

View File

@ -127,7 +127,6 @@ async fn main() -> std::io::Result<()> {
let thread_count = thread_counter();
info!("Running ffplayout API, listen on http://{conn}");
debug!("Use {thread_count} threads for the webserver");
// no 'allow origin' here, give it to the reverse proxy
HttpServer::new(move || {

View File

@ -45,6 +45,7 @@ pub const FFMPEG_UNRECOVERABLE_ERRORS: [&str; 5] = [
];
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputMode {
Desktop,
HLS,
@ -83,6 +84,17 @@ impl FromStr for OutputMode {
}
}
impl fmt::Display for OutputMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
OutputMode::Desktop => write!(f, "desktop"),
OutputMode::HLS => write!(f, "hls"),
OutputMode::Null => write!(f, "null"),
OutputMode::Stream => write!(f, "stream"),
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ProcessMode {
@ -139,7 +151,7 @@ pub struct Source {
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct PlayoutConfig {
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub advanced: AdvancedConfig,
pub general: General,
pub mail: Mail,
@ -156,28 +168,31 @@ pub struct PlayoutConfig {
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct General {
pub help_text: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub id: i32,
#[serde(skip_serializing, skip_deserializing)]
pub channel_id: i32,
pub stop_threshold: f64,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub generate: Option<Vec<String>>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_filters: Vec<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub ffmpeg_libs: Vec<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub template: Option<Template>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub skip_validation: bool,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub validate: bool,
}
impl General {
fn new(channel_id: i32, config: &Configuration) -> Self {
fn new(config: &Configuration) -> Self {
Self {
help_text: config.general_help.clone(),
channel_id,
id: config.id,
channel_id: config.channel_id,
stop_threshold: config.stop_threshold,
generate: None,
ffmpeg_filters: vec![],
@ -281,7 +296,7 @@ pub struct Processing {
pub audio_channels: u8,
pub volume: f64,
pub custom_filter: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub cmd: Option<Vec<String>>,
}
@ -316,9 +331,9 @@ impl Processing {
pub struct Ingest {
pub help_text: String,
pub enable: bool,
input_param: String,
pub input_param: String,
pub custom_filter: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
@ -339,10 +354,10 @@ pub struct Playlist {
pub help_text: String,
pub path: PathBuf,
pub day_start: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub start_sec: Option<f64>,
pub length: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub length_sec: Option<f64>,
pub infinit: bool,
}
@ -365,7 +380,7 @@ impl Playlist {
pub struct Storage {
pub help_text: String,
pub path: PathBuf,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub paths: Vec<PathBuf>,
pub filler: PathBuf,
pub extensions: Vec<String>,
@ -393,11 +408,11 @@ impl Storage {
pub struct Text {
pub help_text: String,
pub add_text: bool,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub node_pos: Option<usize>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub zmq_stream_socket: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub zmq_server_socket: Option<String>,
pub fontfile: String,
pub text_from_filename: bool,
@ -443,11 +458,11 @@ pub struct Output {
pub help_text: String,
pub mode: OutputMode,
pub output_param: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub output_count: usize,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub output_filter: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
@ -474,6 +489,24 @@ pub fn string_to_log_level(l: String) -> Level {
}
}
pub fn string_to_processing_mode(l: String) -> ProcessMode {
match l.to_lowercase().as_str() {
"playlist" => ProcessMode::Playlist,
"folder" => ProcessMode::Folder,
_ => ProcessMode::Playlist,
}
}
pub fn string_to_output_mode(l: String) -> OutputMode {
match l.to_lowercase().as_str() {
"desktop" => OutputMode::Desktop,
"hls" => OutputMode::HLS,
"null" => OutputMode::Null,
"stream" => OutputMode::Stream,
_ => OutputMode::HLS,
}
}
fn default_track_index() -> i32 {
-1
}
@ -496,7 +529,7 @@ impl PlayoutConfig {
.expect("Can't read advanced config");
let advanced = AdvancedConfig::new(adv_config);
let general = General::new(channel, &config);
let general = General::new(&config);
let mail = Mail::new(&config);
let logging = Logging::new(&config);
let mut processing = Processing::new(&config);

View File

@ -32,6 +32,7 @@ struct MediaParams {
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProcessCtl {
Status,
Start,
Stop,
Restart,
@ -42,6 +43,7 @@ impl FromStr for ProcessCtl {
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input.to_lowercase().as_str() {
"status" => Ok(Self::Status),
"start" => Ok(Self::Start),
"stop" => Ok(Self::Stop),
"restart" => Ok(Self::Restart),
@ -53,6 +55,7 @@ impl FromStr for ProcessCtl {
impl fmt::Display for ProcessCtl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Status => write!(f, "status"),
Self::Start => write!(f, "start"),
Self::Stop => write!(f, "stop"),
Self::Restart => write!(f, "restart"),

View File

@ -405,7 +405,6 @@ pub fn init_logging(mail_queues: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>) -> io::
builder
.default(log_level)
.module("actix_files", LevelFilter::Error)
.module("actix_web", LevelFilter::Error)
.module("hyper", LevelFilter::Error)
.module("libc", LevelFilter::Error)
.module("neli", LevelFilter::Error)

View File

@ -63,10 +63,7 @@ CREATE TABLE configurations (
starttls INTEGER NOT NULL DEFAULT 0,
mail_level TEXT NOT NULL DEFAULT "ERROR",
interval INTEGER NOT NULL DEFAULT 120,
logging_help TEXT NOT NULL DEFAULT "If 'log_to_file' is true, log to file, when is false log to console. \n'backup_count' says how long log files will be saved in days.\n'local_time' to false will set log timestamps to UTC. Path to /var/log/ only if you run this program as daemon.\n'level' can be DEBUG, INFO, WARNING, ERROR.\n'ffmpeg_level/ingest_level' can be INFO, WARNING, ERROR.\n'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.\n'ignore_lines' makes logging to ignore strings that contains matched lines, in frontend is a semicolon separated list.",
log_to_file INTEGER NOT NULL DEFAULT 1,
backup_count INTEGER NOT NULL DEFAULT 7,
local_time INTEGER NOT NULL DEFAULT 1,
logging_help TEXT NOT NULL DEFAULT "If 'log_to_file' is true, log to file, when is false log to console. \n'local_time' to false will set log timestamps to UTC. Path to /var/log/ only if you run this program as daemon.\n'level' can be DEBUG, INFO, WARNING, ERROR.\n'ffmpeg_level/ingest_level' can be INFO, WARNING, ERROR.\n'detect_silence' logs an error message if the audio line is silent for 15 seconds during the validation process.\n'ignore_lines' makes logging to ignore strings that contains matched lines, in frontend is a semicolon separated list.",
ffmpeg_level TEXT NOT NULL DEFAULT "ERROR",
ingest_level TEXT NOT NULL DEFAULT "ERROR",
detect_silence INTEGER NOT NULL DEFAULT 1,