add env variables to clap arguments, add paths to global table and change config paths to relative

This commit is contained in:
jb-alvarado 2024-06-13 09:29:55 +02:00
parent 1daea32eb9
commit 933d7a9065
12 changed files with 249 additions and 185 deletions

View File

@ -21,7 +21,7 @@ actix-web-httpauth = "0.8"
actix-web-lab = "0.20" actix-web-lab = "0.20"
argon2 = "0.5" argon2 = "0.5"
chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] } chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] }
clap = { version = "4.3", features = ["derive"] } clap = { version = "4.3", features = ["derive", "env"] }
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
derive_more = "0.99" derive_more = "0.99"
faccess = "0.2" faccess = "0.2"

View File

@ -4,7 +4,10 @@ use chrono::{TimeDelta, Utc};
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::utils::{GlobalSettings, Role}; use crate::{
db::models::{GlobalSettings, Role},
utils::errors::ServiceError,
};
// Token lifetime // Token lifetime
const JWT_EXPIRATION_DAYS: i64 = 7; const JWT_EXPIRATION_DAYS: i64 = 7;
@ -29,17 +32,20 @@ impl Claims {
} }
/// Create a json web token (JWT) /// Create a json web token (JWT)
pub fn create_jwt(claims: Claims) -> Result<String, Error> { pub async fn create_jwt(claims: Claims) -> Result<String, ServiceError> {
let config = GlobalSettings::global(); let config = GlobalSettings::global();
let encoding_key = EncodingKey::from_secret(config.secret.as_bytes()); let encoding_key = EncodingKey::from_secret(config.secret.clone().unwrap().as_bytes());
jsonwebtoken::encode(&Header::default(), &claims, &encoding_key) Ok(jsonwebtoken::encode(
.map_err(|e| ErrorUnauthorized(e.to_string())) &Header::default(),
&claims,
&encoding_key,
)?)
} }
/// Decode a json web token (JWT) /// Decode a json web token (JWT)
pub async fn decode_jwt(token: &str) -> Result<Claims, Error> { pub async fn decode_jwt(token: &str) -> Result<Claims, Error> {
let config = GlobalSettings::global(); let config = GlobalSettings::global();
let decoding_key = DecodingKey::from_secret(config.secret.as_bytes()); let decoding_key = DecodingKey::from_secret(config.secret.clone().unwrap().as_bytes());
jsonwebtoken::decode::<Claims>(token, &decoding_key, &Validation::default()) jsonwebtoken::decode::<Claims>(token, &decoding_key, &Validation::default())
.map(|data| data.claims) .map(|data| data.claims)
.map_err(|e| ErrorUnauthorized(e.to_string())) .map_err(|e| ErrorUnauthorized(e.to_string()))

View File

@ -38,6 +38,7 @@ use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use tokio::fs; use tokio::fs;
use crate::db::models::Role;
use crate::utils::{ use crate::utils::{
channels::{create_channel, delete_channel}, channels::{create_channel, delete_channel},
config::{ config::{
@ -52,7 +53,7 @@ use crate::utils::{
}, },
naive_date_time_from_str, naive_date_time_from_str,
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
public_path, read_log_file, system, Role, TextFilter, public_path, read_log_file, system, TextFilter,
}; };
use crate::vec_strings; use crate::vec_strings;
use crate::{ use crate::{
@ -164,28 +165,34 @@ struct ProgramItem {
#[post("/auth/login/")] #[post("/auth/login/")]
pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder { pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder {
let conn = pool.into_inner(); let conn = pool.into_inner();
match handles::select_login(&conn, &credentials.username).await { let username = credentials.username.clone();
let password = credentials.password.clone();
match handles::select_login(&conn, &username).await {
Ok(mut user) => { Ok(mut user) => {
let role = handles::select_role(&conn, &user.role_id.unwrap_or_default()) let role = handles::select_role(&conn, &user.role_id.unwrap_or_default())
.await .await
.unwrap_or(Role::Guest); .unwrap_or(Role::Guest);
web::block(move || {
let pass = user.password.clone(); let pass = user.password.clone();
let hash = PasswordHash::new(&pass).unwrap(); let password_clone = password.clone();
user.password = "".into(); user.password = "".into();
if Argon2::default() if web::block(move || {
.verify_password(credentials.password.as_bytes(), &hash) let hash = PasswordHash::new(&pass).unwrap();
Argon2::default().verify_password(password_clone.as_bytes(), &hash)
})
.await
.is_ok() .is_ok()
{ {
let claims = Claims::new(user.id, user.username.clone(), role.clone()); let claims = Claims::new(user.id, username.clone(), role.clone());
if let Ok(token) = create_jwt(claims) { if let Ok(token) = create_jwt(claims).await {
user.token = Some(token); user.token = Some(token);
}; };
info!("user {} login, with role: {role}", credentials.username); info!("user {} login, with role: {role}", username);
web::Json(UserObj { web::Json(UserObj {
message: "login correct!".into(), message: "login correct!".into(),
@ -194,7 +201,7 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
.customize() .customize()
.with_status(StatusCode::OK) .with_status(StatusCode::OK)
} else { } else {
error!("Wrong password for {}!", credentials.username); error!("Wrong password for {username}!");
web::Json(UserObj { web::Json(UserObj {
message: "Wrong password!".into(), message: "Wrong password!".into(),
@ -203,14 +210,11 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
.customize() .customize()
.with_status(StatusCode::FORBIDDEN) .with_status(StatusCode::FORBIDDEN)
} }
})
.await
.unwrap()
} }
Err(e) => { Err(e) => {
error!("Login {} failed! {e}", credentials.username); error!("Login {username} failed! {e}");
web::Json(UserObj { web::Json(UserObj {
message: format!("Login {} failed!", credentials.username), message: format!("Login {username} failed!"),
user: None, user: None,
}) })
.customize() .customize()

View File

@ -9,8 +9,8 @@ use sqlx::{sqlite::SqliteQueryResult, Pool, Sqlite};
use tokio::task; use tokio::task;
use super::models::{AdvancedConfiguration, Configuration}; use super::models::{AdvancedConfiguration, Configuration};
use crate::db::models::{Channel, TextPreset, User}; use crate::db::models::{Channel, GlobalSettings, Role, TextPreset, User};
use crate::utils::{local_utc_offset, GlobalSettings, Role}; use crate::utils::local_utc_offset;
pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std::error::Error>> { pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std::error::Error>> {
match sqlx::migrate!("../migrations").run(conn).await { match sqlx::migrate!("../migrations").run(conn).await {
@ -40,7 +40,7 @@ pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std
} }
pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> { pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
let query = "SELECT secret FROM global WHERE id = 1"; let query = "SELECT secret, hls_path, playlist_path, storage_path, logging_path FROM global WHERE id = 1";
sqlx::query_as(query).fetch_one(conn).await sqlx::query_as(query).fetch_one(conn).await
} }

View File

@ -1,11 +1,53 @@
use std::{error::Error, fmt, str::FromStr};
use once_cell::sync::OnceCell;
use regex::Regex; use regex::Regex;
use serde::{ use serde::{
de::{self, Visitor}, de::{self, Visitor},
Deserialize, Serialize, Deserialize, Serialize,
}; };
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
use crate::db::handles;
use crate::utils::config::PlayoutConfig; use crate::utils::config::PlayoutConfig;
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct GlobalSettings {
pub secret: Option<String>,
pub hls_path: String,
pub playlist_path: String,
pub storage_path: String,
pub logging_path: String,
}
impl GlobalSettings {
pub async fn new(conn: &Pool<Sqlite>) -> Self {
let global_settings = handles::select_global(conn);
match global_settings.await {
Ok(g) => g,
Err(_) => GlobalSettings {
secret: None,
hls_path: String::new(),
playlist_path: String::new(),
storage_path: String::new(),
logging_path: String::new(),
},
}
}
pub fn global() -> &'static GlobalSettings {
INSTANCE.get().expect("Config is not initialized")
}
}
static INSTANCE: OnceCell<GlobalSettings> = OnceCell::new();
pub async fn init_globales(conn: &Pool<Sqlite>) {
let config = GlobalSettings::new(conn).await;
INSTANCE.set(config).unwrap();
}
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct User { pub struct User {
#[sqlx(default)] #[sqlx(default)]
@ -45,6 +87,73 @@ impl LoginUser {
} }
} }
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum Role {
GlobalAdmin,
ChannelAdmin,
User,
Guest,
}
impl Role {
pub fn set_role(role: &str) -> Self {
match role {
"global_admin" => Role::GlobalAdmin,
"channel_admin" => Role::ChannelAdmin,
"user" => Role::User,
_ => Role::Guest,
}
}
}
impl FromStr for Role {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"global_admin" => Ok(Self::GlobalAdmin),
"channel_admin" => Ok(Self::ChannelAdmin),
"user" => Ok(Self::User),
_ => Ok(Self::Guest),
}
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::GlobalAdmin => write!(f, "global_admin"),
Self::ChannelAdmin => write!(f, "channel_admin"),
Self::User => write!(f, "user"),
Self::Guest => write!(f, "guest"),
}
}
}
impl<'r> sqlx::decode::Decode<'r, ::sqlx::Sqlite> for Role
where
&'r str: sqlx::decode::Decode<'r, sqlx::Sqlite>,
{
fn decode(
value: <sqlx::Sqlite as sqlx::database::HasValueRef<'r>>::ValueRef,
) -> Result<Role, Box<dyn Error + 'static + Send + Sync>> {
let value = <&str as sqlx::decode::Decode<sqlx::Sqlite>>::decode(value)?;
Ok(value.parse()?)
}
}
impl FromRow<'_, SqliteRow> for Role {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
match row.get("name") {
"global_admin" => Ok(Self::GlobalAdmin),
"channel_admin" => Ok(Self::ChannelAdmin),
"user" => Ok(Self::User),
_ => Ok(Self::Guest),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] #[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)]
pub struct TextPreset { pub struct TextPreset {
#[sqlx(default)] #[sqlx(default)]

View File

@ -21,12 +21,14 @@ use path_clean::PathClean;
use ffplayout::{ use ffplayout::{
api::{auth, routes::*}, api::{auth, routes::*},
db::{db_pool, handles, models::LoginUser}, db::{
db_pool, handles,
models::{init_globales, LoginUser},
},
player::controller::{ChannelController, ChannelManager}, player::controller::{ChannelController, ChannelManager},
sse::{broadcast::Broadcaster, routes::*, AuthState}, sse::{broadcast::Broadcaster, routes::*, AuthState},
utils::{ utils::{
config::PlayoutConfig, config::PlayoutConfig,
init_globales,
logging::{init_logging, MailQueue}, logging::{init_logging, MailQueue},
run_args, run_args,
}, },

View File

@ -5,9 +5,10 @@ use actix_web_grants::proc_macro::protect;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{check_uuid, prune_uuids, AuthState, UuidData}; use super::{check_uuid, prune_uuids, AuthState, UuidData};
use crate::db::models::Role;
use crate::player::controller::ChannelController; use crate::player::controller::ChannelController;
use crate::sse::broadcast::Broadcaster; use crate::sse::broadcast::Broadcaster;
use crate::utils::{errors::ServiceError, Role}; use crate::utils::errors::ServiceError;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct User { struct User {

View File

@ -10,12 +10,13 @@ pub struct Args {
#[clap(short, long, help = "ask for user credentials")] #[clap(short, long, help = "ask for user credentials")]
pub ask: bool, pub ask: bool,
#[clap(long, help = "path to database file")] #[clap(long, env, help = "path to database file")]
pub db: Option<PathBuf>, pub db: Option<PathBuf>,
#[clap( #[clap(
short, short,
long, long,
env,
help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)", help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)",
num_args = 1.., num_args = 1..,
)] )]
@ -24,24 +25,37 @@ pub struct Args {
#[clap(long, help = "List available channel ids")] #[clap(long, help = "List available channel ids")]
pub list_channels: bool, pub list_channels: bool,
#[clap(long, help = "path to public files")] #[clap(long, env, help = "path to public files")]
pub public: Option<PathBuf>, pub public: Option<PathBuf>,
#[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")] #[clap(short, env, long, help = "Listen on IP:PORT, like: 127.0.0.1:8787")]
pub listen: Option<String>, pub listen: Option<String>,
#[clap(long, help = "Keep log file for given days")] #[clap(long, env, help = "Keep log file for given days")]
pub log_backup_count: Option<usize>, pub log_backup_count: Option<usize>,
#[clap(long, help = "Override logging level: trace, debug, info, warn, error")] #[clap(
long,
env,
help = "Override logging level: trace, debug, info, warn, error"
)]
pub log_level: Option<String>, pub log_level: Option<String>,
#[clap(long, help = "Logging path")] #[clap(long, env, help = "Logging path")]
pub log_path: Option<PathBuf>, pub log_path: Option<PathBuf>,
#[clap(long, help = "Log to console")] #[clap(long, env, help = "Log to console")]
pub log_to_console: bool, pub log_to_console: bool,
#[clap(long, env, help = "HLS output path")]
pub hls_path: Option<PathBuf>,
#[clap(long, env, help = "Playlist root path")]
pub playlist_path: Option<PathBuf>,
#[clap(long, env, help = "Storage root path")]
pub storage_path: Option<PathBuf>,
#[clap(short, long, help = "domain name for initialization")] #[clap(short, long, help = "domain name for initialization")]
pub domain: Option<String>, pub domain: Option<String>,

View File

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use shlex::split; use shlex::split;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use crate::db::{handles, models::Configuration}; use crate::db::{handles, models};
use crate::utils::{free_tcp_socket, time_to_sec}; use crate::utils::{free_tcp_socket, time_to_sec};
use crate::vec_strings; use crate::vec_strings;
use crate::AdvancedConfig; use crate::AdvancedConfig;
@ -151,6 +151,8 @@ pub struct Source {
/// This we init ones, when ffplayout is starting and use them globally in the hole program. /// This we init ones, when ffplayout is starting and use them globally in the hole program.
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct PlayoutConfig { pub struct PlayoutConfig {
#[serde(skip_serializing, skip_deserializing)]
pub global: Global,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub advanced: AdvancedConfig, pub advanced: AdvancedConfig,
pub general: General, pub general: General,
@ -165,6 +167,25 @@ pub struct PlayoutConfig {
pub output: Output, pub output: Output,
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Global {
pub hls_path: PathBuf,
pub playlist_path: PathBuf,
pub storage_path: PathBuf,
pub logging_path: PathBuf,
}
impl Global {
pub fn new(config: &models::GlobalSettings) -> Self {
Self {
hls_path: PathBuf::from(config.hls_path.clone()),
playlist_path: PathBuf::from(config.playlist_path.clone()),
storage_path: PathBuf::from(config.storage_path.clone()),
logging_path: PathBuf::from(config.logging_path.clone()),
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct General { pub struct General {
pub help_text: String, pub help_text: String,
@ -188,7 +209,7 @@ pub struct General {
} }
impl General { impl General {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.general_help.clone(), help_text: config.general_help.clone(),
id: config.id, id: config.id,
@ -218,7 +239,7 @@ pub struct Mail {
} }
impl Mail { impl Mail {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.mail_help.clone(), help_text: config.mail_help.clone(),
subject: config.subject.clone(), subject: config.subject.clone(),
@ -259,7 +280,7 @@ pub struct Logging {
} }
impl Logging { impl Logging {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.logging_help.clone(), help_text: config.logging_help.clone(),
ffmpeg_level: config.ffmpeg_level.clone(), ffmpeg_level: config.ffmpeg_level.clone(),
@ -301,7 +322,7 @@ pub struct Processing {
} }
impl Processing { impl Processing {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.processing_help.clone(), help_text: config.processing_help.clone(),
mode: ProcessMode::new(&config.processing_mode.clone()), mode: ProcessMode::new(&config.processing_mode.clone()),
@ -338,7 +359,7 @@ pub struct Ingest {
} }
impl Ingest { impl Ingest {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.ingest_help.clone(), help_text: config.ingest_help.clone(),
enable: config.ingest_enable, enable: config.ingest_enable,
@ -363,7 +384,7 @@ pub struct Playlist {
} }
impl Playlist { impl Playlist {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.playlist_help.clone(), help_text: config.playlist_help.clone(),
path: PathBuf::from(config.playlist_path.clone()), path: PathBuf::from(config.playlist_path.clone()),
@ -388,7 +409,7 @@ pub struct Storage {
} }
impl Storage { impl Storage {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.storage_help.clone(), help_text: config.storage_help.clone(),
path: PathBuf::from(config.storage_path.clone()), path: PathBuf::from(config.storage_path.clone()),
@ -421,7 +442,7 @@ pub struct Text {
} }
impl Text { impl Text {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.text_help.clone(), help_text: config.text_help.clone(),
add_text: config.add_text, add_text: config.add_text,
@ -444,7 +465,7 @@ pub struct Task {
} }
impl Task { impl Task {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.task_help.clone(), help_text: config.task_help.clone(),
enable: config.task_enable, enable: config.task_enable,
@ -467,7 +488,7 @@ pub struct Output {
} }
impl Output { impl Output {
fn new(config: &Configuration) -> Self { fn new(config: &models::Configuration) -> Self {
Self { Self {
help_text: config.output_help.clone(), help_text: config.output_help.clone(),
mode: OutputMode::new(&config.output_mode), mode: OutputMode::new(&config.output_mode),
@ -521,6 +542,9 @@ fn default_track_index() -> i32 {
impl PlayoutConfig { impl PlayoutConfig {
pub async fn new(pool: &Pool<Sqlite>, channel: i32) -> Self { pub async fn new(pool: &Pool<Sqlite>, channel: i32) -> Self {
let global = handles::select_global(pool)
.await
.expect("Can't read globals");
let config = handles::select_configuration(pool, channel) let config = handles::select_configuration(pool, channel)
.await .await
.expect("Can't read config"); .expect("Can't read config");
@ -528,6 +552,7 @@ impl PlayoutConfig {
.await .await
.expect("Can't read advanced config"); .expect("Can't read advanced config");
let global = Global::new(&global);
let advanced = AdvancedConfig::new(adv_config); let advanced = AdvancedConfig::new(adv_config);
let general = General::new(&config); let general = General::new(&config);
let mail = Mail::new(&config); let mail = Mail::new(&config);
@ -644,6 +669,7 @@ impl PlayoutConfig {
} }
Self { Self {
global,
advanced, advanced,
general, general,
mail, mail,

View File

@ -77,6 +77,12 @@ impl From<std::num::ParseIntError> for ServiceError {
} }
} }
impl From<jsonwebtoken::errors::Error> for ServiceError {
fn from(err: jsonwebtoken::errors::Error) -> ServiceError {
ServiceError::Unauthorized(err.to_string())
}
}
impl From<actix_web::error::BlockingError> for ServiceError { impl From<actix_web::error::BlockingError> for ServiceError {
fn from(err: actix_web::error::BlockingError) -> ServiceError { fn from(err: actix_web::error::BlockingError) -> ServiceError {
ServiceError::BadRequest(err.to_string()) ServiceError::BadRequest(err.to_string())

View File

@ -1,18 +1,14 @@
use std::{ use std::{
env, env, fmt,
error::Error,
fmt,
fs::{self, metadata}, fs::{self, metadata},
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
net::TcpListener, net::TcpListener,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr,
}; };
use chrono::{format::ParseErrorKind, prelude::*}; use chrono::{format::ParseErrorKind, prelude::*};
use faccess::PathExt; use faccess::PathExt;
use log::*; use log::*;
use once_cell::sync::OnceCell;
use path_clean::PathClean; use path_clean::PathClean;
use rand::Rng; use rand::Rng;
use regex::Regex; use regex::Regex;
@ -21,9 +17,6 @@ use serde::{
de::{self, Visitor}, de::{self, Visitor},
Deserialize, Deserializer, Serialize, Deserialize, Deserializer, Serialize,
}; };
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
use crate::ARGS;
pub mod advanced_config; pub mod advanced_config;
pub mod args_parse; pub mod args_parse;
@ -38,109 +31,10 @@ pub mod playlist;
pub mod system; pub mod system;
pub mod task_runner; pub mod task_runner;
use crate::db::{ use crate::db::{db_pool, handles::insert_user, models::User};
db_pool,
handles::{insert_user, select_global},
models::User,
};
use crate::player::utils::time_to_sec; use crate::player::utils::time_to_sec;
use crate::utils::{errors::ServiceError, logging::log_file_path}; use crate::utils::{errors::ServiceError, logging::log_file_path};
use crate::ARGS;
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum Role {
GlobalAdmin,
ChannelAdmin,
User,
Guest,
}
impl Role {
pub fn set_role(role: &str) -> Self {
match role {
"global_admin" => Role::GlobalAdmin,
"channel_admin" => Role::ChannelAdmin,
"user" => Role::User,
_ => Role::Guest,
}
}
}
impl FromStr for Role {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"global_admin" => Ok(Self::GlobalAdmin),
"channel_admin" => Ok(Self::ChannelAdmin),
"user" => Ok(Self::User),
_ => Ok(Self::Guest),
}
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::GlobalAdmin => write!(f, "global_admin"),
Self::ChannelAdmin => write!(f, "channel_admin"),
Self::User => write!(f, "user"),
Self::Guest => write!(f, "guest"),
}
}
}
impl<'r> sqlx::decode::Decode<'r, ::sqlx::Sqlite> for Role
where
&'r str: sqlx::decode::Decode<'r, sqlx::Sqlite>,
{
fn decode(
value: <sqlx::Sqlite as sqlx::database::HasValueRef<'r>>::ValueRef,
) -> Result<Role, Box<dyn Error + 'static + Send + Sync>> {
let value = <&str as sqlx::decode::Decode<sqlx::Sqlite>>::decode(value)?;
Ok(value.parse()?)
}
}
impl FromRow<'_, SqliteRow> for Role {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
match row.get("name") {
"global_admin" => Ok(Self::GlobalAdmin),
"channel_admin" => Ok(Self::ChannelAdmin),
"user" => Ok(Self::User),
_ => Ok(Self::Guest),
}
}
}
#[derive(Debug, sqlx::FromRow)]
pub struct GlobalSettings {
pub secret: String,
}
impl GlobalSettings {
async fn new(conn: &Pool<Sqlite>) -> Self {
let global_settings = select_global(conn);
match global_settings.await {
Ok(g) => g,
Err(_) => GlobalSettings {
secret: String::new(),
},
}
}
pub fn global() -> &'static GlobalSettings {
INSTANCE.get().expect("Config is not initialized")
}
}
static INSTANCE: OnceCell<GlobalSettings> = OnceCell::new();
pub async fn init_globales(conn: &Pool<Sqlite>) {
let config = GlobalSettings::new(conn).await;
INSTANCE.set(config).unwrap();
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct TextFilter { pub struct TextFilter {

View File

@ -5,6 +5,10 @@ CREATE TABLE
global ( global (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
secret TEXT NOT NULL, secret TEXT NOT NULL,
hls_path TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public",
playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists",
storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media",
logging_path TEXT NOT NULL DEFAULT "/var/log/ffplayout",
UNIQUE (secret) UNIQUE (secret)
); );
@ -78,7 +82,7 @@ CREATE TABLE
ingest_level TEXT NOT NULL DEFAULT "ERROR", ingest_level TEXT NOT NULL DEFAULT "ERROR",
detect_silence INTEGER NOT NULL DEFAULT 1, detect_silence INTEGER NOT NULL DEFAULT 1,
ignore_lines TEXT NOT NULL DEFAULT "P sub_mb_type 4 out of range at;error while decoding MB;negative number of zero coeffs at;out of range intra chroma pred mode;non-existing SPS 0 referenced in buffering period", ignore_lines TEXT NOT NULL DEFAULT "P sub_mb_type 4 out of range at;error while decoding MB;negative number of zero coeffs at;out of range intra chroma pred mode;non-existing SPS 0 referenced in buffering period",
processing_help TEXT NOT NULL DEFAULT "Default processing for all clips, to have them unique. Mode can be playlist or folder.\n'aspect' must be a float number.'logo' is only used if the path exist.\n'logo_scale' scale the logo to target size, leave it blank when no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' logo can become transparent.\nWith 'audio_tracks' it is possible to configure how many audio tracks should be processed.\n'audio_channels' can be use, if audio has more channels then only stereo.\nWith 'logo_position' in format 'x:y' you set the logo position.\nWith 'custom_filter' it is possible, to apply further filters. The filter outputs should end with [c_v_out] for video filter, and [c_a_out] for audio filter.", processing_help TEXT NOT NULL DEFAULT "Default processing for all clips, to have them unique. Mode can be playlist or folder.\n'aspect' must be a float number.'logo' is only used if the path exist, path is relative to your storage folder.\n'logo_scale' scale the logo to target size, leave it blank when no scaling is needed, format is 'width:height', for example '100:-1' for proportional scaling. With 'logo_opacity' logo can become transparent.\nWith 'audio_tracks' it is possible to configure how many audio tracks should be processed.\n'audio_channels' can be use, if audio has more channels then only stereo.\nWith 'logo_position' in format 'x:y' you set the logo position.\nWith 'custom_filter' it is possible, to apply further filters. The filter outputs should end with [c_v_out] for video filter, and [c_a_out] for audio filter.",
processing_mode TEXT NOT NULL DEFAULT "playlist", processing_mode TEXT NOT NULL DEFAULT "playlist",
audio_only INTEGER NOT NULL DEFAULT 0, audio_only INTEGER NOT NULL DEFAULT 0,
copy_audio INTEGER NOT NULL DEFAULT 0, copy_audio INTEGER NOT NULL DEFAULT 0,
@ -88,7 +92,7 @@ CREATE TABLE
aspect REAL NOT NULL DEFAULT 1.778, aspect REAL NOT NULL DEFAULT 1.778,
fps REAL NOT NULL DEFAULT 25.0, fps REAL NOT NULL DEFAULT 25.0,
add_logo INTEGER NOT NULL DEFAULT 1, add_logo INTEGER NOT NULL DEFAULT 1,
logo TEXT NOT NULL DEFAULT "/usr/share/ffplayout/logo.png", logo TEXT NOT NULL DEFAULT "graphics/logo.png",
logo_scale TEXT NOT NULL DEFAULT "", logo_scale TEXT NOT NULL DEFAULT "",
logo_opacity REAL NOT NULL DEFAULT 0.7, logo_opacity REAL NOT NULL DEFAULT 0.7,
logo_position TEXT NOT NULL DEFAULT "W-w-12:12", logo_position TEXT NOT NULL DEFAULT "W-w-12:12",
@ -102,19 +106,17 @@ CREATE TABLE
ingest_param TEXT NOT NULL DEFAULT "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream", ingest_param TEXT NOT NULL DEFAULT "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream",
ingest_filter TEXT NOT NULL DEFAULT "", ingest_filter TEXT NOT NULL DEFAULT "",
playlist_help TEXT NOT NULL DEFAULT "'path' can be a path to a single file, or a directory. For directory put only the root folder, for example '/playlists', subdirectories are read by the program. Subdirectories needs this structure '/playlists/2018/01'.\n'day_start' means at which time the playlist should start, leave day_start blank when playlist should always start at the begin. 'length' represent the target length from playlist, when is blank real length will not consider.\n'infinit: true' works with single playlist file and loops it infinitely.", playlist_help TEXT NOT NULL DEFAULT "'path' can be a path to a single file, or a directory. For directory put only the root folder, for example '/playlists', subdirectories are read by the program. Subdirectories needs this structure '/playlists/2018/01'.\n'day_start' means at which time the playlist should start, leave day_start blank when playlist should always start at the begin. 'length' represent the target length from playlist, when is blank real length will not consider.\n'infinit: true' works with single playlist file and loops it infinitely.",
playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists",
day_start TEXT NOT NULL DEFAULT "05:59:25", day_start TEXT NOT NULL DEFAULT "05:59:25",
length TEXT NOT NULL DEFAULT "24:00:00", length TEXT NOT NULL DEFAULT "24:00:00",
infinit INTEGER NOT NULL DEFAULT 0, infinit INTEGER NOT NULL DEFAULT 0,
storage_help TEXT NOT NULL DEFAULT "'filler' is for playing instead of a missing file or fill the end to reach 24 hours, can be a file or folder, it will loop when is necessary.\n'extensions' search only files with this extension. Set 'shuffle' to 'true' to pick files randomly.", storage_help TEXT NOT NULL DEFAULT "'filler' is for playing instead of a missing file or fill the end to reach 24 hours, can be a file or folder, it will loop when is necessary.\n'extensions' search only files with this extension. Set 'shuffle' to 'true' to pick files randomly.",
storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media", filler TEXT NOT NULL DEFAULT "filler/filler.mp4",
filler TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media/filler/filler.mp4",
extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm", extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
shuffle INTEGER NOT NULL DEFAULT 1, shuffle INTEGER NOT NULL DEFAULT 1,
text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. On windows fontfile path need to be like this 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.", text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. fontfile is a relative path to your storage folder.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.",
add_text INTEGER NOT NULL DEFAULT 1, add_text INTEGER NOT NULL DEFAULT 1,
text_from_filename INTEGER NOT NULL DEFAULT 0, text_from_filename INTEGER NOT NULL DEFAULT 0,
fontfile TEXT NOT NULL DEFAULT "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", fontfile TEXT NOT NULL DEFAULT "fonts/DejaVuSans.ttf",
style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4", style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4",
regex TEXT NOT NULL DEFAULT "^.+[/\\](.*)(.mp4|.mkv|.webm)$", regex TEXT NOT NULL DEFAULT "^.+[/\\](.*)(.mp4|.mkv|.webm)$",
task_help TEXT NOT NULL DEFAULT "Run an external program with a given media object. The media object is in json format and contains all the information about the current clip. The external program can be a script or a binary, but should only run for a short time.", task_help TEXT NOT NULL DEFAULT "Run an external program with a given media object. The media object is in json format and contains all the information about the current clip. The external program can be a script or a binary, but should only run for a short time.",
@ -122,7 +124,7 @@ CREATE TABLE
task_path TEXT NOT NULL DEFAULT "", task_path TEXT NOT NULL DEFAULT "",
output_help TEXT NOT NULL DEFAULT "The final playout compression. Set the settings to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust 'output_param:' settings when you want to stream to a rtmp/rtsp/srt/... server.\nIn production don't serve hls playlist with ffplayout, use nginx or another web server!", output_help TEXT NOT NULL DEFAULT "The final playout compression. Set the settings to your needs. 'mode' has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust 'output_param:' settings when you want to stream to a rtmp/rtsp/srt/... server.\nIn production don't serve hls playlist with ffplayout, use nginx or another web server!",
output_mode TEXT NOT NULL DEFAULT "hls", output_mode TEXT NOT NULL DEFAULT "hls",
output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename /usr/share/ffplayout/public/live/stream-%d.ts /usr/share/ffplayout/public/live/stream.m3u8", output_param TEXT NOT NULL DEFAULT "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8",
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE
); );