diff --git a/ffplayout-frontend b/ffplayout-frontend index 8d63cc4f..6225a272 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 8d63cc4f85f3cbd530d509d74494b6fefbb9bf2c +Subproject commit 6225a2725465e5ff0d572b368346047e16d1be72 diff --git a/ffplayout/src/api/routes.rs b/ffplayout/src/api/routes.rs index 6d0233b5..302dcf39 100644 --- a/ffplayout/src/api/routes.rs +++ b/ffplayout/src/api/routes.rs @@ -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 { 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>, id: web::Path, - _data: web::Json, + data: web::Json, + controllers: web::Data>, ) -> Result { - 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 diff --git a/ffplayout/src/db/handles.rs b/ffplayout/src/db/handles.rs index bbbe06e5..8cb9481e 100644 --- a/ffplayout/src/db/handles.rs +++ b/ffplayout/src/db/handles.rs @@ -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, + config: Configuration, +) -> Result { + 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, channel: i32, diff --git a/ffplayout/src/db/models.rs b/ffplayout/src/db/models.rs index 464071ec..93a90434 100644 --- a/ffplayout/src/db/models.rs +++ b/ffplayout/src/db/models.rs @@ -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 } diff --git a/ffplayout/src/main.rs b/ffplayout/src/main.rs index b65e5c16..a46310a8 100644 --- a/ffplayout/src/main.rs +++ b/ffplayout/src/main.rs @@ -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 || { diff --git a/ffplayout/src/utils/config.rs b/ffplayout/src/utils/config.rs index 37afaa32..05277379 100644 --- a/ffplayout/src/utils/config.rs +++ b/ffplayout/src/utils/config.rs @@ -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>, - #[serde(skip_serializing)] + #[serde(skip_serializing, skip_deserializing)] pub ffmpeg_filters: Vec, - #[serde(skip_serializing)] + #[serde(skip_serializing, skip_deserializing)] pub ffmpeg_libs: Vec, - #[serde(skip_serializing)] + #[serde(skip_serializing, skip_deserializing)] pub template: Option