diff --git a/.vscode/settings.json b/.vscode/settings.json index e103bcb6..48d90da8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,28 +48,49 @@ }, "cSpell.words": [ "actix", + "aevalsrc", + "afade", + "apad", + "boxborderw", + "boxcolor", "canonicalize", + "cgop", + "coeffs", "ffpengine", "flexi", + "fontcolor", "fontfile", + "fontsize", "httpauth", + "ifnot", + "keyint", "lettre", "libc", + "libx", + "libzmq", "maxrate", "minrate", + "muxdelay", "muxer", + "muxpreload", + "n'vtt", "neli", "nuxt", "paris", "Referer", "reqwest", "rsplit", + "RTSP", "rustls", + "scenecut", "sqlite", "sqlx", "starttls", "tokio", + "tpad", "unistd", - "uuids" + "uuids", + "webm", + "zerolatency" ] } diff --git a/Cargo.lock b/Cargo.lock index 5815f5a6..ec4364b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,7 +1215,7 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ffplayout" -version = "0.24.0-beta5" +version = "0.24.0-rc1" dependencies = [ "actix-files", "actix-multipart", @@ -1939,9 +1939,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -3674,7 +3674,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.24.0-beta5" +version = "0.24.0-rc1" dependencies = [ "actix-rt", "actix-test", @@ -3932,9 +3932,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" diff --git a/Cargo.toml b/Cargo.toml index 38223c9e..5765bcda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] description = "24/7 playout based on rust and ffmpeg" readme = "README.md" -version = "0.24.0-beta5" +version = "0.24.0-rc1" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 1ea0ed4b..296ff9a4 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -27,7 +27,7 @@ crossbeam-channel = "0.5" derive_more = { version = "1", features = ["display"] } faccess = "0.2" ffprobe = "0.4" -flexi_logger = { version = "0.29", features = ["kv", "colors"] } +flexi_logger = { version = "=0.29.0", features = ["kv", "colors"] } futures-util = { version = "0.3", default-features = false, features = ["std"] } home = "0.5" jsonwebtoken = "9" diff --git a/engine/src/db/handles.rs b/engine/src/db/handles.rs index 62c6f87c..6acdc03f 100644 --- a/engine/src/db/handles.rs +++ b/engine/src/db/handles.rs @@ -37,7 +37,7 @@ pub async fn db_migrate(conn: &Pool) -> Result<(), Box) -> Result { let query = - "SELECT id, secret, logs, playlists, public, storage, shared FROM global WHERE id = 1"; + "SELECT id, secret, logs, playlists, public, storage, shared, mail_smtp, mail_user, mail_password, mail_starttls FROM global WHERE id = 1"; sqlx::query_as(query).fetch_one(conn).await } @@ -46,7 +46,9 @@ pub async fn update_global( conn: &Pool, global: GlobalSettings, ) -> Result { - let query = "UPDATE global SET logs = $2, playlists = $3, public = $4, storage = $5, shared = $6 WHERE id = 1"; + let query = + "UPDATE global SET logs = $2, playlists = $3, public = $4, storage = $5, shared = $6, + mail_smtp = $7, mail_user = $8, mail_password = $9, mail_starttls = $10 WHERE id = 1"; sqlx::query(query) .bind(global.id) @@ -55,6 +57,10 @@ pub async fn update_global( .bind(global.public) .bind(global.storage) .bind(global.shared) + .bind(global.mail_smtp) + .bind(global.mail_user) + .bind(global.mail_password) + .bind(global.mail_starttls) .execute(conn) .await } @@ -212,17 +218,13 @@ pub async fn update_configuration( id: i32, config: PlayoutConfig, ) -> Result { - let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_smtp = $4, mail_addr = $5, mail_pass = $6, mail_recipient = $7, mail_starttls = $8, mail_level = $9, mail_interval = $10, logging_ffmpeg_level = $11, logging_ingest_level = $12, logging_detect_silence = $13, logging_ignore = $14, processing_mode = $15, processing_audio_only = $16, processing_copy_audio = $17, processing_copy_video = $18, processing_width = $19, processing_height = $20, processing_aspect = $21, processing_fps = $22, processing_add_logo = $23, processing_logo = $24, processing_logo_scale = $25, processing_logo_opacity = $26, processing_logo_position = $27, processing_audio_tracks = $28, processing_audio_track_index = $29, processing_audio_channels = $30, processing_volume = $31, processing_filter = $32, processing_vtt_enable = $33, processing_vtt_dummy = $34, ingest_enable = $35, ingest_param = $36, ingest_filter = $37, playlist_day_start = $38, playlist_length = $39, playlist_infinit = $40, storage_filler = $41, storage_extensions = $42, storage_shuffle = $43, text_add = $44, text_from_filename = $45, text_font = $46, text_style = $47, text_regex = $48, task_enable = $49, task_path = $50, output_mode = $51, output_param = $52 WHERE id = $1"; + let query = "UPDATE configurations SET general_stop_threshold = $2, mail_subject = $3, mail_recipient = $4, mail_level = $5, mail_interval = $6, logging_ffmpeg_level = $7, logging_ingest_level = $8, logging_detect_silence = $9, logging_ignore = $10, processing_mode = $11, processing_audio_only = $12, processing_copy_audio = $13, processing_copy_video = $14, processing_width = $15, processing_height = $16, processing_aspect = $17, processing_fps = $18, processing_add_logo = $19, processing_logo = $20, processing_logo_scale = $21, processing_logo_opacity = $22, processing_logo_position = $23, processing_audio_tracks = $24, processing_audio_track_index = $25, processing_audio_channels = $26, processing_volume = $27, processing_filter = $28, processing_vtt_enable = $29, processing_vtt_dummy = $30, ingest_enable = $31, ingest_param = $32, ingest_filter = $33, playlist_day_start = $34, playlist_length = $35, playlist_infinit = $36, storage_filler = $37, storage_extensions = $38, storage_shuffle = $39, text_add = $40, text_from_filename = $41, text_font = $42, text_style = $43, text_regex = $44, task_enable = $45, task_path = $46, output_mode = $47, output_param = $48 WHERE id = $1"; sqlx::query(query) .bind(id) .bind(config.general.stop_threshold) .bind(config.mail.subject) - .bind(config.mail.smtp_server) - .bind(config.mail.sender_addr) - .bind(config.mail.sender_pass) .bind(config.mail.recipient) - .bind(config.mail.starttls) .bind(config.mail.mail_level.as_str()) .bind(config.mail.interval) .bind(config.logging.ffmpeg_level) @@ -397,6 +399,39 @@ pub async fn insert_user(conn: &Pool, user: User) -> Result<(), sqlx::Er Ok(()) } +pub async fn insert_or_update_user(conn: &Pool, user: User) -> Result<(), sqlx::Error> { + let password_hash = task::spawn_blocking(move || { + let salt = SaltString::generate(&mut OsRng); + let hash = Argon2::default() + .hash_password(user.password.clone().as_bytes(), &salt) + .unwrap(); + + hash.to_string() + }) + .await + .unwrap(); + + let query = "INSERT INTO user (mail, username, password, role_id) VALUES($1, $2, $3, $4) + ON CONFLICT(username) DO UPDATE SET + mail = excluded.mail, username = excluded.username, password = excluded.password, role_id = excluded.role_id + RETURNING id"; + + let user_id: i32 = sqlx::query(query) + .bind(user.mail) + .bind(user.username) + .bind(password_hash) + .bind(user.role_id) + .fetch_one(conn) + .await? + .get("id"); + + if let Some(channel_ids) = user.channel_ids { + insert_user_channel(conn, user_id, channel_ids).await?; + } + + Ok(()) +} + pub async fn update_user( conn: &Pool, id: i32, @@ -493,7 +528,7 @@ pub async fn new_channel_presets( channel_id: i32, ) -> Result { let query = "INSERT INTO presets (name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id) - VALUES ('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', $1), + VALUES ('Default', 'Welcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', $1), ('Empty Text', '', '0', '0', '24', '4', '#000000', '0', '#000000', '0', '0', $1), ('Bottom Text fade in', 'The upcoming event will be delayed by a few minutes.', '(w-text_w)/2', '(h-line_h)*0.9', '24', '4', '#ffffff', '1', '#000000@0x80', '4', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))', $1), ('Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9', '24', '4', '#ffffff', '1', '#000000@0x80', '4', '1.0', $1);"; diff --git a/engine/src/db/models.rs b/engine/src/db/models.rs index 2bfd4557..2f9abc29 100644 --- a/engine/src/db/models.rs +++ b/engine/src/db/models.rs @@ -12,7 +12,7 @@ use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite}; use crate::db::handles; use crate::utils::config::PlayoutConfig; -#[derive(Clone, Debug, Deserialize, Serialize, sqlx::FromRow)] +#[derive(Clone, Default, Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct GlobalSettings { pub id: i32, pub secret: Option, @@ -21,6 +21,10 @@ pub struct GlobalSettings { pub public: String, pub storage: String, pub shared: bool, + pub mail_smtp: String, + pub mail_user: String, + pub mail_password: String, + pub mail_starttls: bool, } impl GlobalSettings { @@ -37,6 +41,10 @@ impl GlobalSettings { public: String::new(), storage: String::new(), shared: false, + mail_smtp: String::new(), + mail_user: String::new(), + mail_password: String::new(), + mail_starttls: false, }, } } @@ -264,11 +272,7 @@ pub struct Configuration { pub mail_help: String, pub mail_subject: String, - pub mail_smtp: String, - pub mail_addr: String, - pub mail_pass: String, pub mail_recipient: String, - pub mail_starttls: bool, pub mail_level: String, pub mail_interval: i64, @@ -348,10 +352,6 @@ impl Configuration { general_stop_threshold: config.general.stop_threshold, mail_help: config.mail.help_text, mail_subject: config.mail.subject, - mail_smtp: config.mail.smtp_server, - mail_starttls: config.mail.starttls, - mail_addr: config.mail.sender_addr, - mail_pass: config.mail.sender_pass, mail_recipient: config.mail.recipient, mail_level: config.mail.mail_level.to_string(), mail_interval: config.mail.interval, diff --git a/engine/src/utils/args_parse.rs b/engine/src/utils/args_parse.rs index 0e74666a..0b71f328 100644 --- a/engine/src/utils/args_parse.rs +++ b/engine/src/utils/args_parse.rs @@ -15,7 +15,7 @@ use tokio::fs; use crate::db::{ handles, - models::{Channel, GlobalSettings, User}, + models::{Channel, User}, }; use crate::utils::{ advanced_config::AdvancedConfig, @@ -59,6 +59,18 @@ pub struct Args { #[clap(long, env, help_heading = Some("Initial Setup"), help = "Storage root path")] pub storage: Option, + #[clap(long, env, help_heading = Some("Initial Setup"), help = "SMTP server for system mails")] + pub mail_smtp: Option, + + #[clap(long, env, help_heading = Some("Initial Setup"), help = "Mail user for system mails")] + pub mail_user: Option, + + #[clap(long, env, help_heading = Some("Initial Setup"), help = "Mail password for system mails")] + pub mail_password: Option, + + #[clap(long, env, help_heading = Some("Initial Setup"), help = "Use TLS for system mails")] + pub mail_starttls: bool, + #[clap( long, env, @@ -76,8 +88,8 @@ pub struct Args { #[clap(long, help_heading = Some("Initial Setup / Playlist"), help = "Path to playlist, or playlist root folder.")] pub playlists: Option, - #[clap(short, long, help_heading = Some("General"), help = "Add a global admin")] - pub add: bool, + #[clap(long, help_heading = Some("General"), help = "Add or update a global admin use")] + pub user_set: bool, #[clap(long, env, help_heading = Some("General"), help = "Path to database file")] pub db: Option, @@ -240,15 +252,10 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { let mut logging = String::new(); let mut public = String::new(); let mut shared_store = String::new(); - let mut global = GlobalSettings { - id: 0, - secret: None, - logs: String::new(), - playlists: String::new(), - public: String::new(), - storage: String::new(), - shared: false, - }; + let mut mail_smtp = String::new(); + let mut mail_user = String::new(); + let mut mail_starttls = String::new(); + let mut global = handles::select_global(pool).await.map_err(|_| 1)?; if check_user.unwrap_or_default().is_empty() { global_user(&mut args); @@ -257,94 +264,156 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { if let Some(st) = args.storage { global.storage = st; } else { - print!("Storage path [/var/lib/ffplayout/tv-media]: "); + print!("Storage path [{}]: ", global.storage); stdout().flush().unwrap(); - stdin() .read_line(&mut storage) .expect("Did not enter a correct path?"); - global.storage = if storage.trim().is_empty() { - "/var/lib/ffplayout/tv-media".to_string() - } else { - storage + if !storage.trim().is_empty() { + global.storage = storage .trim() .trim_matches(|c| c == '"' || c == '\'') - .to_string() - }; + .to_string(); + } } if let Some(pl) = args.playlists { global.playlists = pl } else { - print!("Playlist path [/var/lib/ffplayout/playlists]: "); + print!("Playlist path [{}]: ", global.playlists); stdout().flush().unwrap(); - stdin() .read_line(&mut playlist) .expect("Did not enter a correct path?"); - global.playlists = if playlist.trim().is_empty() { - "/var/lib/ffplayout/playlists".to_string() - } else { - playlist + if !playlist.trim().is_empty() { + global.playlists = playlist .trim() .trim_matches(|c| c == '"' || c == '\'') - .to_string() - }; + .to_string(); + } } if let Some(lp) = args.logs { global.logs = lp; } else { - print!("Logging path [/var/log/ffplayout]: "); + print!("Logging path [{}]: ", global.logs); stdout().flush().unwrap(); - stdin() .read_line(&mut logging) .expect("Did not enter a correct path?"); - global.logs = if logging.trim().is_empty() { - "/var/log/ffplayout".to_string() - } else { - logging + if !logging.trim().is_empty() { + global.logs = logging .trim() .trim_matches(|c| c == '"' || c == '\'') - .to_string() + .to_string(); } } if let Some(p) = args.public { global.public = p; } else { - print!("Public (HLS) path [/usr/share/ffplayout/public]: "); + print!("Public (HLS) path [{}]: ", global.public); stdout().flush().unwrap(); - stdin() .read_line(&mut public) .expect("Did not enter a correct path?"); - global.public = if public.trim().is_empty() { - "/usr/share/ffplayout/public".to_string() - } else { - public + if !public.trim().is_empty() { + global.public = public .trim() .trim_matches(|c| c == '"' || c == '\'') - .to_string() - }; + .to_string(); + } } if args.shared { global.shared = true; } else { - print!("Shared storage [Y/n]: "); + print!( + "Shared storage [{}]: ", + if global.shared { "yes" } else { "no" } + ); stdout().flush().unwrap(); - stdin() .read_line(&mut shared_store) .expect("Did not enter a yes or no?"); - global.shared = shared_store.trim().to_lowercase().starts_with('y'); + if !shared_store.trim().is_empty() { + global.shared = shared_store.trim().to_lowercase().starts_with('y'); + } + } + + if let Some(smtp) = args.mail_smtp { + global.mail_smtp = smtp; + } else { + print!("SMTP server [{}]: ", global.mail_smtp); + stdout().flush().unwrap(); + stdin() + .read_line(&mut mail_smtp) + .expect("Did not enter a correct SMTP server?"); + + if !mail_smtp.trim().is_empty() { + global.mail_smtp = mail_smtp + .trim() + .trim_matches(|c| c == '"' || c == '\'') + .to_string(); + } + } + + if let Some(user) = args.mail_user { + global.mail_user = user; + } else { + print!("SMTP user [{}]: ", global.mail_user); + stdout().flush().unwrap(); + stdin() + .read_line(&mut mail_user) + .expect("Did not enter a correct SMTP user?"); + + if !mail_user.trim().is_empty() { + global.mail_user = mail_user + .trim() + .trim_matches(|c| c == '"' || c == '\'') + .to_string(); + } + } + + if let Some(pass) = args.mail_password { + global.mail_password = pass; + } else { + print!( + "SMTP password [{}]: ", + if global.mail_password.is_empty() { + "" + } else { + "********" + } + ); + stdout().flush().unwrap(); + let password = read_password().unwrap_or_default(); + + if !password.trim().is_empty() { + global.mail_password = password.trim().to_string(); + } + } + + if args.mail_starttls { + global.mail_starttls = true; + } else { + print!( + "SMTP use TLS [{}]: ", + if global.mail_starttls { "yes" } else { "no" } + ); + stdout().flush().unwrap(); + stdin() + .read_line(&mut mail_starttls) + .expect("Did not enter a yes or no?"); + + if !mail_starttls.trim().is_empty() { + global.mail_starttls = mail_starttls.trim().to_lowercase().starts_with('y'); + } } if let Err(e) = handles::update_global(pool, global.clone()).await { @@ -385,7 +454,7 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { } println!("\nSet global settings done..."); - } else if args.add { + } else if args.user_set { global_user(&mut args); } @@ -404,12 +473,12 @@ pub async fn run_args(pool: &Pool) -> Result<(), i32> { token: None, }; - if let Err(e) = handles::insert_user(pool, ff_user).await { + if let Err(e) = handles::insert_or_update_user(pool, ff_user).await { eprintln!("{e}"); error_code = 1; }; - println!("Create global admin user \"{username}\" done..."); + println!("Create/update global admin user \"{username}\" done..."); } if ARGS.list_channels { diff --git a/engine/src/utils/config.rs b/engine/src/utils/config.rs index e0292671..6d39024d 100644 --- a/engine/src/utils/config.rs +++ b/engine/src/utils/config.rs @@ -43,12 +43,13 @@ pub const FFMPEG_IGNORE_ERRORS: [&str; 13] = [ "frame size not set", ]; -pub const FFMPEG_UNRECOVERABLE_ERRORS: [&str; 5] = [ +pub const FFMPEG_UNRECOVERABLE_ERRORS: [&str; 6] = [ "Address already in use", "Invalid argument", "Numerical result", "Error initializing complex filters", "Error while decoding stream #0:0: Invalid data found when processing input", + "Unrecognized option", ]; #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] @@ -239,9 +240,13 @@ impl General { pub struct Mail { pub help_text: String, pub subject: String, + #[serde(skip_serializing, skip_deserializing)] pub smtp_server: String, + #[serde(skip_serializing, skip_deserializing)] pub starttls: bool, + #[serde(skip_serializing, skip_deserializing)] pub sender_addr: String, + #[serde(skip_serializing, skip_deserializing)] pub sender_pass: String, pub recipient: String, pub mail_level: Level, @@ -249,14 +254,14 @@ pub struct Mail { } impl Mail { - fn new(config: &models::Configuration) -> Self { + fn new(global: &models::GlobalSettings, config: &models::Configuration) -> Self { Self { help_text: config.mail_help.clone(), subject: config.mail_subject.clone(), - smtp_server: config.mail_smtp.clone(), - starttls: config.mail_starttls, - sender_addr: config.mail_addr.clone(), - sender_pass: config.mail_pass.clone(), + smtp_server: global.mail_smtp.clone(), + starttls: global.mail_starttls, + sender_addr: global.mail_user.clone(), + sender_pass: global.mail_password.clone(), recipient: config.mail_recipient.clone(), mail_level: string_to_log_level(config.mail_level.clone()), interval: config.mail_interval, @@ -570,9 +575,7 @@ fn default_track_index() -> i32 { impl PlayoutConfig { pub async fn new(pool: &Pool, channel_id: i32) -> Result { - let global = handles::select_global(pool) - .await - .expect("Can't read globals"); + let global = handles::select_global(pool).await?; let channel = handles::select_channel(pool, &channel_id).await?; let config = handles::select_configuration(pool, channel_id).await?; let adv_config = handles::select_advanced_configuration(pool, channel_id).await?; @@ -580,7 +583,7 @@ impl PlayoutConfig { let channel = Channel::new(&global, channel); let advanced = AdvancedConfig::new(adv_config); let general = General::new(&config); - let mail = Mail::new(&config); + let mail = Mail::new(&global, &config); let logging = Logging::new(&config); let mut processing = Processing::new(&config); let mut ingest = Ingest::new(&config); @@ -919,12 +922,28 @@ pub async fn get_config( if args.shared { // config.channel.shared could be true already, // so should not be overridden with false when args.shared is not set - config.channel.shared = args.shared + config.channel.shared = true } if let Some(volume) = args.volume { config.processing.volume = volume; } + if let Some(mail_smtp) = args.mail_smtp { + config.mail.smtp_server = mail_smtp; + } + + if let Some(mail_user) = args.mail_user { + config.mail.sender_addr = mail_user; + } + + if let Some(mail_password) = args.mail_password { + config.mail.sender_pass = mail_password; + } + + if args.mail_starttls { + config.mail.starttls = true; + } + Ok(config) } diff --git a/engine/src/utils/logging.rs b/engine/src/utils/logging.rs index 4dc7e1bf..61ebd5ba 100644 --- a/engine/src/utils/logging.rs +++ b/engine/src/utils/logging.rs @@ -396,7 +396,7 @@ pub fn mail_queue(mail_queues: Arc>>>>) { poisoned.into_inner() }); - let expire = round_to_nearest_ten(q_lock.config.interval); + let expire = round_to_nearest_ten(q_lock.config.interval.max(30)); if interval % expire == 0 && !q_lock.is_empty() { if q_lock.config.recipient.contains('@') { diff --git a/frontend/components/GenericModal.vue b/frontend/components/GenericModal.vue index 129bb15a..e989b15f 100644 --- a/frontend/components/GenericModal.vue +++ b/frontend/components/GenericModal.vue @@ -1,6 +1,6 @@