diff --git a/Cargo.lock b/Cargo.lock index 05336316..3583ddb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,7 +626,7 @@ dependencies = [ [[package]] name = "chrono" version = "0.4.20-beta.1" -source = "git+https://github.com/chronotope/chrono.git#051e1170c41477ce162301c8711110a4577c1a23" +source = "git+https://github.com/chronotope/chrono.git#187819ff43e0e4da351b3ea4ac2d3076e06e8251" dependencies = [ "num-integer", "num-traits", @@ -636,9 +636,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.8" +version = "3.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d" dependencies = [ "atty", "bitflags", @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157" dependencies = [ "cache-padded", ] @@ -880,9 +880,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ccfd8c0ee4cce11e45b3fd6f9d5e69e0cc62912aa6a0cb1bf4617b0eba5a12f" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -1009,7 +1009,7 @@ dependencies = [ [[package]] name = "ffplayout" -version = "0.10.5" +version = "0.11.0" dependencies = [ "clap", "crossbeam-channel 0.5.5", @@ -1027,7 +1027,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.4.2" +version = "0.5.0" dependencies = [ "actix-multipart", "actix-web", @@ -1056,7 +1056,7 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.10.4" +version = "0.10.5" dependencies = [ "chrono 0.4.20-beta.1", "crossbeam-channel 0.5.5", @@ -1135,7 +1135,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.3", + "spin 0.9.4", ] [[package]] @@ -1406,9 +1406,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashlink" @@ -1543,7 +1543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown 0.12.2", + "hashbrown 0.12.3", ] [[package]] @@ -2084,9 +2084,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" [[package]] name = "paris" @@ -2167,9 +2167,9 @@ checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "pem" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ "base64", ] @@ -2548,18 +2548,18 @@ checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" [[package]] name = "serde" -version = "1.0.138" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.138" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" dependencies = [ "proc-macro2", "quote", @@ -2591,9 +2591,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec0091e1f5aa338283ce049bd9dfefd55e1f168ac233e85c1ffe0038fb48cbe" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", @@ -2692,9 +2692,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" dependencies = [ "lock_api", ] @@ -2932,10 +2932,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.19.2" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" dependencies = [ + "autocfg", "bytes", "libc", "memchr", @@ -3070,9 +3071,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" @@ -3391,7 +3392,7 @@ dependencies = [ [[package]] name = "zeromq" version = "0.3.3" -source = "git+https://github.com/zeromq/zmq.rs.git#9e0eb7c16950146d285d952939ea8d5a5fc812c9" +source = "git+https://github.com/zeromq/zmq.rs.git#c7cbd1c0d589c9d671626c7fa5ba26843c0ab219" dependencies = [ "async-std", "async-trait", diff --git a/assets/11-ffplayout b/assets/11-ffplayout index d333da91..440b44f9 100644 --- a/assets/11-ffplayout +++ b/assets/11-ffplayout @@ -1,3 +1,5 @@ -# give user www-data permission to control the ffplayout systemd service +# give user ffpu permission to control the ffplayout systemd service -www-data ALL = NOPASSWD: /bin/systemctl start ffplayout.service, /bin/systemctl stop ffplayout.service, /bin/systemctl reload ffplayout.service, /bin/systemctl restart ffplayout.service, /bin/systemctl status ffplayout.service, /bin/systemctl is-active ffplayout.service +ffpu ALL = NOPASSWD: /usr/bin/systemctl start ffplayout.service, /usr/bin/systemctl stop ffplayout.service, /usr/bin/systemctl restart ffplayout.service, /usr/bin/systemctl status ffplayout.service, /usr/bin/systemctl is-active ffplayout.service, /usr/bin/systemctl enable ffplayout.service, /usr/bin/systemctl disable ffplayout.service + +ffpu ALL = NOPASSWD: /usr/bin/systemctl start ffplayout@*, /usr/bin/systemctl stop ffplayout@*, /usr/bin/systemctl restart ffplayout@*, /usr/bin/systemctl status ffplayout@*, /usr/bin/systemctl is-active ffplayout@*, /usr/bin/systemctl enable ffplayout@*, /usr/bin/systemctl disable ffplayout@* diff --git a/assets/ffpapi.service b/assets/ffpapi.service index b1168fc8..7efe360d 100644 --- a/assets/ffpapi.service +++ b/assets/ffpapi.service @@ -6,8 +6,8 @@ After=network.target remote-fs.target ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8000 Restart=always RestartSec=1 -User=www-data -Group=www-data +User=ffpu +Group=ffpu [Install] WantedBy=multi-user.target diff --git a/assets/ffplayout.service b/assets/ffplayout.service index 34005a41..4cdb7690 100644 --- a/assets/ffplayout.service +++ b/assets/ffplayout.service @@ -7,8 +7,8 @@ ExecStart=/usr/bin/ffplayout Restart=always RestartSec=1 KillMode=mixed -User=www-data -Group=www-data +User=ffpu +Group=ffpu [Install] WantedBy=multi-user.target diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index 19163885..dba10637 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -85,7 +85,7 @@ playlist: should always start at the begin. 'length' represent the target length from playlist, when is blank real length will not consider. 'infinit true' works with single playlist file and loops it infinitely. - path: /playlists + path: /var/lib/ffplayout/playlists day_start: "5:59:25" length: "24:00:00" infinit: false @@ -94,8 +94,8 @@ storage: help_text: Play ordered or randomly files from path. 'filler_clip' is for fill the end to reach 24 hours, it will loop when is necessary. 'extensions' search only files with this extension. Set 'shuffle' to 'True' to pick files randomly. - path: "/mediaStorage" - filler_clip: "/mediaStorage/filler/filler.mp4" + path: "/var/lib/ffplayout/tv-media" + filler_clip: "/var/lib/ffplayout/tv-media/filler/filler.mp4" extensions: - "mp4" - "mkv" diff --git a/assets/ffplayout@.service b/assets/ffplayout@.service new file mode 100644 index 00000000..695cc38d --- /dev/null +++ b/assets/ffplayout@.service @@ -0,0 +1,14 @@ +[Unit] +Description=Rust and ffmpeg based multi channel playout solution +After=network.target remote-fs.target + +[Service] +ExecStart=/usr/bin/ffplayout %I +Restart=always +RestartSec=1 +KillMode=mixed +User=ffpu +Group=ffpu + +[Install] +WantedBy=multi-user.target diff --git a/debian/postinst b/debian/postinst index 9f1147dd..3725dd5c 100644 --- a/debian/postinst +++ b/debian/postinst @@ -1,27 +1,25 @@ #DEBHELPER# +sysUser="ffpu" + +if ! id $sysUser &>/dev/null; then + adduser --system $sysUser +fi + if [ ! -d "/usr/share/ffplayout/db" ]; then mkdir "/usr/share/ffplayout/db" - chmod 777 "/usr/share/ffplayout/db" + mkdir "/var/lib/ffplayout/playlists" + mkdir "/var/lib/ffplayout/tv-media" /usr/bin/ffpapi -i - if id "www-data" &>/dev/null; then - chown www-data. "/usr/share/ffplayout/db/ffplayout.db" - else - sed -i "s|www-data|root|g" /lib/systemd/system/ffpapi.service - sed -i "s|www-data|root|g" /lib/systemd/system/ffplayout.service - rm -f /etc/sudoers.d/11-ffplayout - - systemctl daemon-reload - fi + chown -R ${sysUser}. "/usr/share/ffplayout" + chown -R ${sysUser}. "/var/lib/ffplayout" + chown -R ${sysUser}. "/etc/ffplayout" fi if [ ! -d "/var/log/ffplayout" ]; then - mkdir /var/log/ffplayout + mkdir "/var/log/ffplayout" - if id "www-data" &>/dev/null; then - chown www-data. /var/log/ffplayout - chown -R www-data. /etc/ffplayout - fi + chown ${sysUser}. "/var/log/ffplayout" fi diff --git a/docs/api.md b/docs/api.md index bf822ebb..0c9d32de 100644 --- a/docs/api.md +++ b/docs/api.md @@ -55,10 +55,10 @@ curl -X POST 'http://localhost:8000/api/user/' -H 'Content-Type: application/jso #### ffpapi Settings -**Get Settings** +**Get Settings from Channel** ```BASH -curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer " +curl -X GET http://127.0.0.1:8000/api/channel/1 -H "Authorization: Bearer " ``` **Response:** @@ -66,7 +66,7 @@ curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer " +curl -X GET http://127.0.0.1:8000/api/channels -H "Authorization: Bearer " ``` -**Update Settings** +**Update Channel** ```BASH -curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \ --d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ -"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", -"role_id": 1, "channel_id": 1 }' \ +curl -X PATCH http://127.0.0.1:8000/api/channel/1 -H "Content-Type: application/json" \ +-d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ +"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \ -H "Authorization: Bearer " ``` +**Create new Channel** + +```BASH +curl -X POST http://127.0.0.1:8000/api/channel/ -H "Content-Type: application/json" \ +-d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \ +"config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png", +"timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \ +-H "Authorization: Bearer " +``` + +**Delete Channel** + +```BASH +curl -X DELETE http://127.0.0.1:8000/api/channel/2 -H "Authorization: Bearer " +``` + #### ffplayout Config **Get Config** diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index 156e6192..e153be17 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -4,7 +4,7 @@ description = "Rest API for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.4.2" +version = "0.5.0" edition = "2021" [dependencies] diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index f100277d..9523b8b6 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -15,11 +15,11 @@ use utils::{ auth, db_path, init_config, models::LoginUser, routes::{ - add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset, file_browser, - gen_playlist, get_all_settings, get_log, get_playlist, get_playout_config, get_presets, - get_settings, get_user, login, media_current, media_last, media_next, move_rename, - patch_settings, process_control, remove, save_file, save_playlist, send_text_message, - update_playout_config, update_preset, update_user, + add_channel, add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset, + file_browser, gen_playlist, get_all_channels, get_channel, get_log, get_playlist, + get_playout_config, get_presets, get_user, login, media_current, media_last, media_next, + move_rename, patch_channel, process_control, remove, remove_channel, save_file, + save_playlist, send_text_message, update_playout_config, update_preset, update_user, }, run_args, Role, }; @@ -84,9 +84,11 @@ async fn main() -> std::io::Result<()> { .service(get_presets) .service(update_preset) .service(delete_preset) - .service(get_settings) - .service(get_all_settings) - .service(patch_settings) + .service(get_channel) + .service(get_all_channels) + .service(patch_channel) + .service(add_channel) + .service(remove_channel) .service(update_user) .service(send_text_message) .service(control_playout) diff --git a/ffplayout-api/src/utils/channels.rs b/ffplayout-api/src/utils/channels.rs new file mode 100644 index 00000000..9c6c145b --- /dev/null +++ b/ffplayout-api/src/utils/channels.rs @@ -0,0 +1,44 @@ +use std::fs; + +use simplelog::*; + +use crate::utils::{ + control::control_service, + errors::ServiceError, + handles::{db_add_channel, db_delete_channel, db_get_channel}, + models::Channel, +}; + +pub async fn create_channel(target_channel: Channel) -> Result { + if !target_channel.service.starts_with("ffplayout@") { + return Err(ServiceError::BadRequest("Bad service name!".to_string())); + } + + if !target_channel.config_path.starts_with("/etc/ffplayout") { + return Err(ServiceError::BadRequest("Bad config path!".to_string())); + } + + fs::copy( + "/usr/share/ffplayout/ffplayout.yml.orig", + &target_channel.config_path, + )?; + + let new_channel = db_add_channel(target_channel).await?; + control_service(new_channel.id, "enable").await?; + + Ok(new_channel) +} + +pub async fn delete_channel(id: i64) -> Result<(), ServiceError> { + let channel = db_get_channel(&id).await?; + control_service(channel.id, "stop").await?; + control_service(channel.id, "disable").await?; + + if let Err(e) = fs::remove_file(channel.config_path) { + error!("{e}"); + }; + + db_delete_channel(&id).await?; + + Ok(()) +} diff --git a/ffplayout-api/src/utils/control.rs b/ffplayout-api/src/utils/control.rs index 8b04e0bf..8f5b0539 100644 --- a/ffplayout-api/src/utils/control.rs +++ b/ffplayout-api/src/utils/control.rs @@ -7,7 +7,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use simplelog::*; -use crate::utils::{errors::ServiceError, handles::db_get_settings, playout_config}; +use crate::utils::{errors::ServiceError, handles::db_get_channel, playout_config}; use ffplayout_lib::vec_strings; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -57,14 +57,32 @@ struct SystemD { impl SystemD { async fn new(id: i64) -> Result { - let settings = db_get_settings(&id).await?; + let channel = db_get_channel(&id).await?; Ok(Self { - service: settings.service, - cmd: vec_strings!["systemctl"], + service: channel.service, + cmd: vec_strings!["/usr/bin/systemctl"], }) } + fn enable(mut self) -> Result { + self.cmd + .append(&mut vec!["enable".to_string(), self.service]); + + Command::new("sudo").args(self.cmd).spawn()?; + + Ok("Success".to_string()) + } + + fn disable(mut self) -> Result { + self.cmd + .append(&mut vec!["disable".to_string(), self.service]); + + Command::new("sudo").args(self.cmd).spawn()?; + + Ok("Success".to_string()) + } + fn start(mut self) -> Result { self.cmd .append(&mut vec!["start".to_string(), self.service]); @@ -167,6 +185,8 @@ pub async fn control_service(id: i64, command: &str) -> Result system_d.enable(), + "disable" => system_d.disable(), "start" => system_d.start(), "stop" => system_d.stop(), "restart" => system_d.restart(), diff --git a/ffplayout-api/src/utils/handles.rs b/ffplayout-api/src/utils/handles.rs index a8d31429..6ab132e0 100644 --- a/ffplayout-api/src/utils/handles.rs +++ b/ffplayout-api/src/utils/handles.rs @@ -9,7 +9,7 @@ use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, Sq use crate::utils::{ db_path, - models::{Settings, TextPreset, User}, + models::{Channel, TextPreset, User}, GlobalSettings, }; @@ -33,16 +33,16 @@ async fn create_schema() -> Result { name TEXT NOT NULL, UNIQUE(name) ); - CREATE TABLE IF NOT EXISTS settings + CREATE TABLE IF NOT EXISTS channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_name TEXT NOT NULL, + name TEXT NOT NULL, preview_url TEXT NOT NULL, config_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, timezone TEXT NOT NULL, service TEXT NOT NULL, - UNIQUE(channel_name) + UNIQUE(name, service) ); CREATE TABLE IF NOT EXISTS presets ( @@ -59,7 +59,7 @@ async fn create_schema() -> Result { boxborderw TEXT NOT NULL, alpha TEXT NOT NULL, channel_id INTEGER NOT NULL DEFAULT 1, - FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL, + FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(name) ); CREATE TABLE IF NOT EXISTS user @@ -72,7 +72,7 @@ async fn create_schema() -> Result { role_id INTEGER NOT NULL DEFAULT 2, channel_id INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL, - FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL, + FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(mail, username) );"; let result = sqlx::query(query).execute(&conn).await; @@ -111,7 +111,7 @@ pub async fn db_init(domain: Option) -> Result<&'static str, Box Result { Ok(result) } -pub async fn db_get_settings(id: &i64) -> Result { +pub async fn db_get_channel(id: &i64) -> Result { let conn = db_connection().await?; - let query = "SELECT * FROM settings WHERE id = $1"; - let result: Settings = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; + let query = "SELECT * FROM channels WHERE id = $1"; + let result: Channel = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; conn.close().await; Ok(result) } -pub async fn db_get_all_settings() -> Result, sqlx::Error> { +pub async fn db_get_all_channels() -> Result, sqlx::Error> { let conn = db_connection().await?; - let query = "SELECT * FROM settings"; - let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; + let query = "SELECT * FROM channels"; + let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; conn.close().await; Ok(result) } -pub async fn db_update_settings( +pub async fn db_update_channel( id: i64, - settings: Settings, + channel: Channel, ) -> Result { let conn = db_connection().await?; - let query = "UPDATE settings SET channel_name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1"; + let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5, timezone = $6 WHERE id = $1"; let result: SqliteQueryResult = sqlx::query(query) .bind(id) - .bind(settings.channel_name.clone()) - .bind(settings.preview_url.clone()) - .bind(settings.config_path.clone()) - .bind(settings.extra_extensions.clone()) + .bind(channel.name) + .bind(channel.preview_url) + .bind(channel.config_path) + .bind(channel.extra_extensions) + .bind(channel.timezone) .execute(&conn) .await?; conn.close().await; @@ -185,6 +186,38 @@ pub async fn db_update_settings( Ok(result) } +pub async fn db_add_channel(channel: Channel) -> Result { + let conn = db_connection().await?; + + let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, timezone, service) VALUES($1, $2, $3, $4, $5, $6)"; + let result = sqlx::query(query) + .bind(channel.name) + .bind(channel.preview_url) + .bind(channel.config_path) + .bind(channel.extra_extensions) + .bind(channel.timezone) + .bind(channel.service) + .execute(&conn) + .await?; + let new_channel: Channel = sqlx::query_as("SELECT * FROM channels WHERE id = $1") + .bind(result.last_insert_rowid()) + .fetch_one(&conn) + .await?; + conn.close().await; + + Ok(new_channel) +} + +pub async fn db_delete_channel(id: &i64) -> Result { + let conn = db_connection().await?; + + let query = "DELETE FROM channels WHERE id = $1"; + let result: SqliteQueryResult = sqlx::query(query).bind(id).execute(&conn).await?; + conn.close().await; + + Ok(result) +} + pub async fn db_role(id: &i64) -> Result { let conn = db_connection().await?; let query = "SELECT name FROM roles WHERE id = $1"; diff --git a/ffplayout-api/src/utils/mod.rs b/ffplayout-api/src/utils/mod.rs index 0615695b..6141cf7b 100644 --- a/ffplayout-api/src/utils/mod.rs +++ b/ffplayout-api/src/utils/mod.rs @@ -12,6 +12,7 @@ use simplelog::*; pub mod args_parse; pub mod auth; +pub mod channels; pub mod control; pub mod errors; pub mod files; @@ -23,8 +24,8 @@ pub mod routes; use crate::utils::{ args_parse::Args, errors::ServiceError, - handles::{db_add_user, db_get_settings, db_global, db_init}, - models::{Settings, User}, + handles::{db_add_user, db_get_channel, db_global, db_init}, + models::{Channel, User}, }; use ffplayout_lib::utils::PlayoutConfig; @@ -183,10 +184,10 @@ pub fn read_playout_config(path: &str) -> Result> Ok(config) } -pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings), ServiceError> { - if let Ok(settings) = db_get_settings(channel_id).await { - if let Ok(config) = read_playout_config(&settings.config_path.clone()) { - return Ok((config, settings)); +pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Channel), ServiceError> { + if let Ok(channel) = db_get_channel(channel_id).await { + if let Ok(config) = read_playout_config(&channel.config_path.clone()) { + return Ok((config, channel)); } } @@ -196,7 +197,7 @@ pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings } pub async fn read_log_file(channel_id: &i64, date: &str) -> Result { - if let Ok(settings) = db_get_settings(channel_id).await { + if let Ok(channel) = db_get_channel(channel_id).await { let mut date_str = "".to_string(); if !date.is_empty() { @@ -204,7 +205,7 @@ pub async fn read_log_file(channel_id: &i64, date: &str) -> Result Result Result { - let (mut config, settings) = playout_config(&id).await?; + let (mut config, channel) = playout_config(&id).await?; config.general.generate = Some(vec![date.clone()]); - match playlist_generator(&config, Some(settings.channel_name)) { + match playlist_generator(&config, Some(channel.name)) { Ok(playlists) => { if !playlists.is_empty() { Ok(playlists[0].clone()) diff --git a/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs index b0572961..6f96ebd7 100644 --- a/ffplayout-api/src/utils/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -22,6 +22,7 @@ use simplelog::*; use crate::utils::{ auth::{create_jwt, Claims}, + channels::{create_channel, delete_channel}, control::{control_service, control_state, media_info, send_message, Process}, errors::ServiceError, files::{ @@ -29,11 +30,11 @@ use crate::utils::{ PathObject, }, handles::{ - db_add_preset, db_add_user, db_delete_preset, db_get_all_settings, db_get_presets, - db_get_settings, db_get_user, db_login, db_role, db_update_preset, db_update_settings, + db_add_preset, db_add_user, db_delete_preset, db_get_all_channels, db_get_channel, + db_get_presets, db_get_user, db_login, db_role, db_update_channel, db_update_preset, db_update_user, }, - models::{LoginUser, Settings, TextPreset, User}, + models::{Channel, LoginUser, TextPreset, User}, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, read_log_file, read_playout_config, Role, }; @@ -219,10 +220,10 @@ async fn add_user(data: web::Json) -> Result /// #### ffpapi Settings /// -/// **Get Settings** +/// **Get Settings from Channel** /// /// ```BASH -/// curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer " +/// curl -X GET http://127.0.0.1:8000/api/channel/1 -H "Authorization: Bearer " /// ``` /// /// **Response:** @@ -230,7 +231,7 @@ async fn add_user(data: web::Json) -> Result /// ```JSON /// { /// "id": 1, -/// "channel_name": "Channel 1", +/// "name": "Channel 1", /// "preview_url": "http://localhost/live/preview.m3u8", /// "config_path": "/etc/ffplayout/ffplayout.yml", /// "extra_extensions": "jpg,jpeg,png", @@ -238,53 +239,85 @@ async fn add_user(data: web::Json) -> Result /// "service": "ffplayout.service" /// } /// ``` -#[get("/settings/{id}")] +#[get("/channel/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn get_settings(id: web::Path) -> Result { - if let Ok(settings) = db_get_settings(&id).await { - return Ok(web::Json(settings)); +async fn get_channel(id: web::Path) -> Result { + if let Ok(channel) = db_get_channel(&id).await { + return Ok(web::Json(channel)); } Err(ServiceError::InternalServerError) } -/// **Get all Settings** +/// **Get settings from all Channels** /// /// ```BASH -/// curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer " +/// curl -X GET http://127.0.0.1:8000/api/channels -H "Authorization: Bearer " /// ``` -#[get("/settings")] +#[get("/channels")] #[has_any_role("Role::Admin", type = "Role")] -async fn get_all_settings() -> Result { - if let Ok(settings) = db_get_all_settings().await { - return Ok(web::Json(settings)); +async fn get_all_channels() -> Result { + if let Ok(channel) = db_get_all_channels().await { + return Ok(web::Json(channel)); } Err(ServiceError::InternalServerError) } -/// **Update Settings** +/// **Update Channel** /// /// ```BASH -/// curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \ -/// -d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ -/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", -/// "role_id": 1, "channel_id": 1 }' \ +/// curl -X PATCH http://127.0.0.1:8000/api/channel/1 -H "Content-Type: application/json" \ +/// -d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ +/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \ /// -H "Authorization: Bearer " /// ``` -#[patch("/settings/{id}")] +#[patch("/channel/{id}")] #[has_any_role("Role::Admin", type = "Role")] -async fn patch_settings( +async fn patch_channel( id: web::Path, - data: web::Json, + data: web::Json, ) -> Result { - if db_update_settings(*id, data.into_inner()).await.is_ok() { + if db_update_channel(*id, data.into_inner()).await.is_ok() { return Ok("Update Success"); }; Err(ServiceError::InternalServerError) } +/// **Create new Channel** +/// +/// ```BASH +/// curl -X POST http://127.0.0.1:8000/api/channel/ -H "Content-Type: application/json" \ +/// -d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \ +/// "config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png", +/// "timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \ +/// -H "Authorization: Bearer " +/// ``` +#[post("/channel/")] +#[has_any_role("Role::Admin", type = "Role")] +async fn add_channel(data: web::Json) -> Result { + match create_channel(data.into_inner()).await { + Ok(c) => Ok(web::Json(c)), + Err(e) => Err(e), + } +} + +/// **Delete Channel** +/// +/// ```BASH +/// curl -X DELETE http://127.0.0.1:8000/api/channel/2 -H "Authorization: Bearer " +/// ``` +#[delete("/channel/{id}")] +#[has_any_role("Role::Admin", type = "Role")] +async fn remove_channel(id: web::Path) -> Result { + if delete_channel(*id).await.is_ok() { + return Ok("Delete Channel Success"); + } + + Err(ServiceError::InternalServerError) +} + /// #### ffplayout Config /// /// **Get Config** @@ -300,8 +333,8 @@ async fn get_playout_config( id: web::Path, _details: AuthDetails, ) -> Result { - if let Ok(settings) = db_get_settings(&id).await { - if let Ok(config) = read_playout_config(&settings.config_path) { + if let Ok(channel) = db_get_channel(&id).await { + if let Ok(config) = read_playout_config(&channel.config_path) { return Ok(web::Json(config)); } }; @@ -321,11 +354,11 @@ async fn update_playout_config( id: web::Path, data: web::Json, ) -> Result { - if let Ok(settings) = db_get_settings(&id).await { + if let Ok(channel) = db_get_channel(&id).await { if let Ok(f) = std::fs::OpenOptions::new() .write(true) .truncate(true) - .open(&settings.config_path) + .open(&channel.config_path) { serde_yaml::to_writer(f, &data).unwrap(); diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 15e0da07..98cde9ee 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.10.5" +version = "0.11.0" edition = "2021" [dependencies] @@ -48,9 +48,11 @@ assets = [ "755" ], ["../assets/ffpapi.service", "/lib/systemd/system/", "644"], + ["../assets/ffplayout@.service", "/lib/systemd/system/", "644"], ["../assets/11-ffplayout", "/etc/sudoers.d/", "644"], ["../assets/ffplayout.yml", "/etc/ffplayout/", "644"], ["../assets/logo.png", "/usr/share/ffplayout/", "644"], + ["../assets/ffplayout.yml", "/usr/share/ffplayout/ffplayout.yml.orig", "644"], ["../README.md", "/usr/share/doc/ffplayout/README", "644"], ] maintainer-scripts = "../debian/" @@ -66,10 +68,12 @@ assets = [ { source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, { source = "../assets/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" }, { source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" }, + { source = "../assets/ffplayout@.service", dest = "/lib/systemd/system/ffplayout@.service", mode = "644" }, { source = "../assets/11-ffplayout", dest = "/etc/sudoers.d/11-ffplayout", mode = "644" }, { source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, { source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, + { source = "../assets/ffplayout.yml", dest = "/usr/share/ffplayout/ffplayout.yml.orig", mode = "644" }, { source = "../debian/postinst", dest = "/usr/share/ffplayout/postinst", mode = "755" }, ] auto-req = "no" diff --git a/ffplayout-engine/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs index ef0550ba..497767c3 100644 --- a/ffplayout-engine/src/utils/arg_parse.rs +++ b/ffplayout-engine/src/utils/arg_parse.rs @@ -6,6 +6,9 @@ use clap::Parser; override_usage = "Run without any command to use config file only, or with commands to override parameters:\n\n ffplayout [OPTIONS]", long_about = None)] pub struct Args { + #[clap(index = 1, value_parser)] + pub channel: Option, + #[clap(short, long, help = "File path to ffplayout.yml")] pub config: Option, @@ -15,7 +18,7 @@ pub struct Args { #[clap( short, long, - help = "Generate playlist for date or date-range, like: 2022-01-01 - 2022-01-10", + help = "Generate playlist for dates, like: 2022-01-01 - 2022-01-10", name = "YYYY-MM-DD", multiple_values = true )] diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index 2aa9d0d7..e8cc08d3 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; pub mod arg_parse; @@ -6,7 +6,20 @@ pub use arg_parse::Args; use ffplayout_lib::utils::{time_to_sec, PlayoutConfig}; pub fn get_config(args: Args) -> PlayoutConfig { - let mut config = PlayoutConfig::new(args.config); + let cfg_path = match args.channel { + Some(c) => { + let path = PathBuf::from(format!("/etc/ffplayout/{c}.yml")); + + if path.is_file() { + Some(path.display().to_string()) + } else { + println!("no file"); + args.config + } + } + None => args.config, + }; + let mut config = PlayoutConfig::new(cfg_path); if let Some(gen) = args.generate { config.general.generate = Some(gen); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 13353080..ea65a76a 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -4,7 +4,7 @@ description = "Library for ffplayout" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.10.4" +version = "0.10.5" edition = "2021" [dependencies] diff --git a/lib/src/utils/logging.rs b/lib/src/utils/logging.rs index b2f74f60..466e85e4 100644 --- a/lib/src/utils/logging.rs +++ b/lib/src/utils/logging.rs @@ -188,6 +188,7 @@ pub fn init_logging( .add_filter_ignore_str("hyper") .add_filter_ignore_str("sqlx") .add_filter_ignore_str("reqwest") + .add_filter_ignore_str("rpc") .set_level_padding(LevelPadding::Left) .set_time_level(time_level) .clone();