start with API

This commit is contained in:
jb-alvarado 2022-06-06 23:07:11 +02:00
parent 63d88fbf97
commit 227d09bce7
16 changed files with 1160 additions and 75 deletions

1
.gitignore vendored
View File

@ -17,5 +17,6 @@
*tar.gz
*.deb
*.rpm
/assets/*.db*
.vscode/

809
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,11 @@ edition = "2021"
default-run = "ffplayout"
[dependencies]
actix-web = "4"
chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" }
clap = { version = "3.1", features = ["derive"] }
crossbeam-channel = "0.5"
faccess = "0.2"
ffprobe = "0.3"
file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" }
jsonrpc-http-server = "18.0"
@ -21,11 +23,14 @@ notify = "4.0"
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", features = ["blocking"] }
sha-crypt = { version = "0.4", features = ["simple"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
shlex = "1.1"
simplelog = { version = "^0.12", features = ["paris"] }
sqlx = { version = "0.5", features = [ "chrono", "runtime-actix-native-tls", "sqlite" ] }
time = { version = "0.3", features = ["formatting", "macros"] }
walkdir = "2"
@ -36,6 +41,10 @@ openssl = { version = "0.10", features = ["vendored"] }
name = "ffplayout"
path = "src/main.rs"
[[bin]]
name = "ffpapi"
path = "src/bin/ffpapi.rs"
[profile.release]
opt-level = 3
strip = true

102
src/api/handles.rs Normal file
View File

@ -0,0 +1,102 @@
use std::path::Path;
use faccess::PathExt;
use simplelog::*;
use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool};
pub fn db_path() -> Result<String, Box<dyn std::error::Error>> {
let sys_path = Path::new("/usr/share/ffplayout");
let mut db_path = String::from("./ffplayout.db");
if sys_path.is_dir() && sys_path.writable() {
db_path = String::from("/usr/share/ffplayout/ffplayout.db");
} else if Path::new("./assets").is_dir() {
db_path = String::from("./assets/ffplayout.db");
}
Ok(db_path)
}
async fn cretea_schema() -> Result<SqliteQueryResult, sqlx::Error> {
let pool = db_connection().await?;
let query = "PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS groups
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS settings
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_name TEXT NOT NULL,
preview_url TEXT NOT NULL,
settings_path TEXT NOT NULL,
extra_extensions TEXT NOT NULL,
UNIQUE(channel_name)
);
CREATE TABLE IF NOT EXISTS user
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
group_id INTEGER NOT NULL DEFAULT 2,
FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(email, username)
);";
let result = sqlx::query(query).execute(&pool).await;
pool.close().await;
result
}
pub async fn db_init() -> Result<&'static str, Box<dyn std::error::Error>> {
let db_path = db_path()?;
if !Sqlite::database_exists(&db_path).await.unwrap_or(false) {
Sqlite::create_database(&db_path).await.unwrap();
match cretea_schema().await {
Ok(_) => info!("Database created Successfully"),
Err(e) => panic!("{e}"),
}
}
let instances = db_connection().await?;
let query = "INSERT INTO groups(name) VALUES('admin'), ('user');
INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions)
VALUES('Channel 1', 'http://localhost/live/preview.m3u8',
'/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');";
sqlx::query(query).execute(&instances).await?;
instances.close().await;
Ok("Database initialized!")
}
pub async fn db_connection() -> Result<Pool<Sqlite>, sqlx::Error> {
let db_path = db_path().unwrap();
let pool = SqlitePool::connect(&db_path).await?;
Ok(pool)
}
pub async fn add_user(
instances: &SqlitePool,
mail: &str,
user: &str,
pass: &str,
group: &i64,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "INSERT INTO user (email, username, password, group_id) VALUES($1, $2, $3, $4)";
let result = sqlx::query(query)
.bind(mail)
.bind(user)
.bind(pass)
.bind(group)
.execute(instances)
.await?;
Ok(result)
}

3
src/api/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod handles;
pub mod models;
pub mod routes;

17
src/api/models.rs Normal file
View File

@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct User {
pub email: String,
pub username: String,
pub password: String,
pub group_id: i64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Settings {
pub channel_name: String,
pub preview_url: String,
pub settings_path: String,
pub extra_extensions: String,
}

41
src/api/routes.rs Normal file
View File

@ -0,0 +1,41 @@
use crate::api::{
handles::{add_user, db_connection},
models::User,
};
use actix_web::{get, post, web, Responder};
use sha_crypt::{sha512_simple, Sha512Params};
#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
format!("Hello {name}!")
}
/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/
#[post("/api/user/")]
pub async fn user(user: web::Json<User>) -> impl Responder {
let params = Sha512Params::new(10_000).expect("RandomError!");
let hashed_password = sha512_simple(&user.password, &params).expect("Should not fail");
// // Verifying a stored password
// assert!(sha512_check("Not so secure password", &hashed_password).is_ok());
if let Ok(pool) = db_connection().await {
if let Err(e) = add_user(
&pool,
&user.email,
&user.username,
&hashed_password,
&user.group_id,
)
.await
{
pool.close().await;
return e.to_string();
};
pool.close().await;
}
format!("User {} added", user.username)
}

109
src/bin/ffpapi.rs Normal file
View File

@ -0,0 +1,109 @@
use std::process::exit;
use actix_web::{App, HttpServer};
use clap::Parser;
use sha_crypt::{sha512_simple, Sha512Params};
use simplelog::*;
use ffplayout_engine::{
api::{
handles::{add_user, db_connection, db_init},
routes::user,
},
utils::{init_logging, GlobalConfig},
};
#[derive(Parser, Debug)]
#[clap(version,
name = "ffpapi",
version = "0.1.0",
about = "ffplayout REST API",
long_about = None)]
pub struct Args {
#[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8080")]
pub listen: Option<String>,
#[clap(short, long, help = "Initialize Database")]
pub init: bool,
#[clap(short, long, help = "Create admin user")]
pub username: Option<String>,
#[clap(short, long, help = "Admin email")]
pub email: Option<String>,
#[clap(short, long, help = "Admin password")]
pub password: Option<String>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args = Args::parse();
if !args.init && args.listen.is_none() && args.username.is_none() {
error!("Wrong number of arguments! Run ffpapi --help for more information.");
exit(1);
}
let mut config = GlobalConfig::new(None);
config.mail.recipient = String::new();
config.logging.log_to_file = false;
let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap();
if args.init {
if let Err(e) = db_init().await {
panic!("{e}");
};
exit(0);
}
if let Some(username) = args.username {
if args.email.is_none() || args.password.is_none() {
error!("Email/password missing!");
exit(1);
}
let params = Sha512Params::new(10_000).expect("RandomError!");
let hashed_password =
sha512_simple(&args.password.unwrap(), &params).expect("Should not fail");
match db_connection().await {
Ok(pool) => {
if let Err(e) =
add_user(&pool, &args.email.unwrap(), &username, &hashed_password, &1).await
{
pool.close().await;
error!("{e}");
exit(1);
};
pool.close().await;
info!("Create admin user \"{username}\" done...");
exit(0);
}
Err(e) => {
panic!("{e}")
}
}
}
if let Some(conn) = args.listen {
let ip_port = conn.split(':').collect::<Vec<&str>>();
let addr = ip_port[0];
let port = ip_port[1].parse::<u16>().unwrap();
info!("running ffplayout API, listen on {conn}");
HttpServer::new(|| App::new().service(user))
.bind((addr, port))?
.run()
.await
} else {
panic!("Run ffpapi with listen parameter!")
}
}

View File

@ -1,6 +1,7 @@
extern crate log;
extern crate simplelog;
pub mod api;
pub mod filter;
pub mod input;
pub mod macros;

View File

@ -14,8 +14,8 @@ use ffplayout_engine::{
output::{player, write_hls},
rpc::json_rpc_server,
utils::{
generate_playlist, init_logging, send_mail, validate_ffmpeg, GlobalConfig, PlayerControl,
PlayoutStatus, ProcessControl,
generate_playlist, get_args, init_logging, send_mail, validate_ffmpeg, GlobalConfig,
PlayerControl, PlayoutStatus, ProcessControl,
},
};
@ -56,7 +56,8 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) {
}
fn main() {
let config = GlobalConfig::new();
let args = get_args();
let config = GlobalConfig::new(Some(args));
let config_clone = config.clone();
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
@ -67,7 +68,7 @@ fn main() {
let proc_ctl2 = proc_control.clone();
let messages = Arc::new(Mutex::new(Vec::new()));
let logging = init_logging(&config, proc_ctl1, messages.clone());
let logging = init_logging(&config, Some(proc_ctl1), Some(messages.clone()));
CombinedLogger::init(logging).unwrap();
validate_ffmpeg(&config);

View File

@ -1,5 +1,4 @@
use std::{
sync::{Arc, Mutex},
thread::{self, sleep},
time::Duration,
};
@ -22,26 +21,24 @@ fn timed_kill(sec: u64, mut proc_ctl: ProcessControl) {
#[test]
#[ignore]
fn playlist_change_at_midnight() {
let mut config = GlobalConfig::new();
let mut config = GlobalConfig::new(None);
config.mail.recipient = "".into();
config.processing.mode = "playlist".into();
config.playlist.day_start = "00:00:00".into();
config.playlist.length = "24:00:00".into();
config.logging.log_to_file = false;
let messages = Arc::new(Mutex::new(Vec::new()));
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
let proc_control = ProcessControl::new();
let proc_ctl = proc_control.clone();
let proc_ctl2 = proc_control.clone();
let logging = init_logging(&config, proc_ctl, messages);
let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap();
mock_time::set_mock_time("2022-05-09T23:59:45");
thread::spawn(move || timed_kill(30, proc_ctl2));
thread::spawn(move || timed_kill(30, proc_ctl));
player(&config, play_control, playout_stat, proc_control);
}
@ -49,26 +46,24 @@ fn playlist_change_at_midnight() {
#[test]
#[ignore]
fn playlist_change_at_six() {
let mut config = GlobalConfig::new();
let mut config = GlobalConfig::new(None);
config.mail.recipient = "".into();
config.processing.mode = "playlist".into();
config.playlist.day_start = "06:00:00".into();
config.playlist.length = "24:00:00".into();
config.logging.log_to_file = false;
let messages = Arc::new(Mutex::new(Vec::new()));
let play_control = PlayerControl::new();
let playout_stat = PlayoutStatus::new();
let proc_control = ProcessControl::new();
let proc_ctl = proc_control.clone();
let proc_ctl2 = proc_control.clone();
let logging = init_logging(&config, proc_ctl, messages);
let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap();
mock_time::set_mock_time("2022-05-09T05:59:45");
thread::spawn(move || timed_kill(30, proc_ctl2));
thread::spawn(move || timed_kill(30, proc_ctl));
player(&config, play_control, playout_stat, proc_control);
}

View File

@ -39,7 +39,7 @@ fn get_date_tomorrow() {
#[test]
fn test_delta() {
let mut config = GlobalConfig::new();
let mut config = GlobalConfig::new(None);
config.mail.recipient = "".into();
config.processing.mode = "playlist".into();
config.playlist.day_start = "00:00:00".into();

View File

@ -1,6 +1,6 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Clone)]
#[clap(version,
about = "ffplayout, Rust based 24/7 playout solution.",
override_usage = "Run without any command to use config file only, or with commands to override parameters:\n\n ffplayout [OPTIONS]",

View File

@ -8,7 +8,7 @@ use std::{
use serde::{Deserialize, Serialize};
use shlex::split;
use crate::utils::{get_args, time_to_sec};
use crate::utils::{time_to_sec, Args};
use crate::vec_strings;
/// Global Config
@ -136,11 +136,10 @@ pub struct Out {
impl GlobalConfig {
/// Read config from YAML file, and set some extra config values.
pub fn new() -> Self {
let args = get_args();
pub fn new(args: Option<Args>) -> Self {
let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
if let Some(cfg) = args.config {
if let Some(cfg) = args.clone().and_then(|a| a.config) {
config_path = PathBuf::from(cfg);
}
@ -219,55 +218,57 @@ impl GlobalConfig {
// Read command line arguments, and override the config with them.
if let Some(gen) = args.generate {
config.general.generate = Some(gen);
}
if let Some(log_path) = args.log {
if Path::new(&log_path).is_dir() {
config.logging.log_to_file = true;
if let Some(arg) = args {
if let Some(gen) = arg.generate {
config.general.generate = Some(gen);
}
config.logging.log_path = log_path;
}
if let Some(playlist) = args.playlist {
config.playlist.path = playlist;
}
if let Some(mode) = args.play_mode {
config.processing.mode = mode;
}
if let Some(folder) = args.folder {
config.storage.path = folder;
config.processing.mode = "folder".into();
}
if let Some(start) = args.start {
config.playlist.day_start = start.clone();
config.playlist.start_sec = Some(time_to_sec(&start));
}
if let Some(length) = args.length {
config.playlist.length = length.clone();
if length.contains(':') {
config.playlist.length_sec = Some(time_to_sec(&length));
} else {
config.playlist.length_sec = Some(86400.0);
if let Some(log_path) = arg.log {
if Path::new(&log_path).is_dir() {
config.logging.log_to_file = true;
}
config.logging.log_path = log_path;
}
}
if args.infinit {
config.playlist.infinit = args.infinit;
}
if let Some(playlist) = arg.playlist {
config.playlist.path = playlist;
}
if let Some(output) = args.output {
config.out.mode = output;
}
if let Some(mode) = arg.play_mode {
config.processing.mode = mode;
}
if let Some(volume) = args.volume {
config.processing.volume = volume;
if let Some(folder) = arg.folder {
config.storage.path = folder;
config.processing.mode = "folder".into();
}
if let Some(start) = arg.start {
config.playlist.day_start = start.clone();
config.playlist.start_sec = Some(time_to_sec(&start));
}
if let Some(length) = arg.length {
config.playlist.length = length.clone();
if length.contains(':') {
config.playlist.length_sec = Some(time_to_sec(&length));
} else {
config.playlist.length_sec = Some(86400.0);
}
}
if arg.infinit {
config.playlist.infinit = arg.infinit;
}
if let Some(output) = arg.output {
config.out.mode = output;
}
if let Some(volume) = arg.volume {
config.processing.volume = volume;
}
}
config
@ -276,7 +277,7 @@ impl GlobalConfig {
impl Default for GlobalConfig {
fn default() -> Self {
Self::new()
Self::new(None)
}
}

View File

@ -167,8 +167,8 @@ fn clean_string(text: &str) -> String {
/// - mail logger
pub fn init_logging(
config: &GlobalConfig,
proc_ctl: ProcessControl,
messages: Arc<Mutex<Vec<String>>>,
proc_ctl: Option<ProcessControl>,
messages: Option<Arc<Mutex<Vec<String>>>>,
) -> Vec<Box<dyn SharedLogger>> {
let config_clone = config.clone();
let app_config = config.logging.clone();
@ -182,6 +182,8 @@ pub fn init_logging(
let mut log_config = ConfigBuilder::new()
.set_thread_level(LevelFilter::Off)
.set_target_level(LevelFilter::Off)
.add_filter_ignore_str("sqlx")
.add_filter_ignore_str("reqwest")
.set_level_padding(LevelPadding::Left)
.set_time_level(time_level)
.clone();
@ -247,10 +249,12 @@ pub fn init_logging(
// set mail logger only the recipient is set in config
if config.mail.recipient.contains('@') && config.mail.recipient.contains('.') {
let messages_clone = messages.clone();
let messages_clone = messages.clone().unwrap();
let interval = config.mail.interval;
thread::spawn(move || mail_queue(config_clone, proc_ctl, messages_clone, interval));
thread::spawn(move || {
mail_queue(config_clone, proc_ctl.unwrap(), messages_clone, interval)
});
let mail_config = log_config.build();
@ -260,7 +264,7 @@ pub fn init_logging(
_ => LevelFilter::Error,
};
app_logger.push(LogMailer::new(filter, mail_config, messages));
app_logger.push(LogMailer::new(filter, mail_config, messages.unwrap()));
}
app_logger

View File

@ -23,7 +23,7 @@ pub mod json_serializer;
mod json_validate;
mod logging;
pub use arg_parse::get_args;
pub use arg_parse::{get_args, Args};
pub use config::GlobalConfig;
pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*};
pub use generator::generate_playlist;