add env variables to clap arguments, add paths to global table and change config paths to relative
This commit is contained in:
parent
1daea32eb9
commit
933d7a9065
@ -21,7 +21,7 @@ actix-web-httpauth = "0.8"
|
||||
actix-web-lab = "0.20"
|
||||
argon2 = "0.5"
|
||||
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"
|
||||
derive_more = "0.99"
|
||||
faccess = "0.2"
|
||||
|
@ -4,7 +4,10 @@ use chrono::{TimeDelta, Utc};
|
||||
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::utils::{GlobalSettings, Role};
|
||||
use crate::{
|
||||
db::models::{GlobalSettings, Role},
|
||||
utils::errors::ServiceError,
|
||||
};
|
||||
|
||||
// Token lifetime
|
||||
const JWT_EXPIRATION_DAYS: i64 = 7;
|
||||
@ -29,17 +32,20 @@ impl Claims {
|
||||
}
|
||||
|
||||
/// 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 encoding_key = EncodingKey::from_secret(config.secret.as_bytes());
|
||||
jsonwebtoken::encode(&Header::default(), &claims, &encoding_key)
|
||||
.map_err(|e| ErrorUnauthorized(e.to_string()))
|
||||
let encoding_key = EncodingKey::from_secret(config.secret.clone().unwrap().as_bytes());
|
||||
Ok(jsonwebtoken::encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&encoding_key,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Decode a json web token (JWT)
|
||||
pub async fn decode_jwt(token: &str) -> Result<Claims, Error> {
|
||||
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())
|
||||
.map(|data| data.claims)
|
||||
.map_err(|e| ErrorUnauthorized(e.to_string()))
|
||||
|
@ -38,6 +38,7 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::db::models::Role;
|
||||
use crate::utils::{
|
||||
channels::{create_channel, delete_channel},
|
||||
config::{
|
||||
@ -52,7 +53,7 @@ use crate::utils::{
|
||||
},
|
||||
naive_date_time_from_str,
|
||||
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::{
|
||||
@ -164,53 +165,56 @@ struct ProgramItem {
|
||||
#[post("/auth/login/")]
|
||||
pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder {
|
||||
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) => {
|
||||
let role = handles::select_role(&conn, &user.role_id.unwrap_or_default())
|
||||
.await
|
||||
.unwrap_or(Role::Guest);
|
||||
|
||||
web::block(move || {
|
||||
let pass = user.password.clone();
|
||||
let pass = user.password.clone();
|
||||
let password_clone = password.clone();
|
||||
|
||||
user.password = "".into();
|
||||
|
||||
if web::block(move || {
|
||||
let hash = PasswordHash::new(&pass).unwrap();
|
||||
user.password = "".into();
|
||||
|
||||
if Argon2::default()
|
||||
.verify_password(credentials.password.as_bytes(), &hash)
|
||||
.is_ok()
|
||||
{
|
||||
let claims = Claims::new(user.id, user.username.clone(), role.clone());
|
||||
|
||||
if let Ok(token) = create_jwt(claims) {
|
||||
user.token = Some(token);
|
||||
};
|
||||
|
||||
info!("user {} login, with role: {role}", credentials.username);
|
||||
|
||||
web::Json(UserObj {
|
||||
message: "login correct!".into(),
|
||||
user: Some(user),
|
||||
})
|
||||
.customize()
|
||||
.with_status(StatusCode::OK)
|
||||
} else {
|
||||
error!("Wrong password for {}!", credentials.username);
|
||||
|
||||
web::Json(UserObj {
|
||||
message: "Wrong password!".into(),
|
||||
user: None,
|
||||
})
|
||||
.customize()
|
||||
.with_status(StatusCode::FORBIDDEN)
|
||||
}
|
||||
Argon2::default().verify_password(password_clone.as_bytes(), &hash)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.is_ok()
|
||||
{
|
||||
let claims = Claims::new(user.id, username.clone(), role.clone());
|
||||
|
||||
if let Ok(token) = create_jwt(claims).await {
|
||||
user.token = Some(token);
|
||||
};
|
||||
|
||||
info!("user {} login, with role: {role}", username);
|
||||
|
||||
web::Json(UserObj {
|
||||
message: "login correct!".into(),
|
||||
user: Some(user),
|
||||
})
|
||||
.customize()
|
||||
.with_status(StatusCode::OK)
|
||||
} else {
|
||||
error!("Wrong password for {username}!");
|
||||
|
||||
web::Json(UserObj {
|
||||
message: "Wrong password!".into(),
|
||||
user: None,
|
||||
})
|
||||
.customize()
|
||||
.with_status(StatusCode::FORBIDDEN)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Login {} failed! {e}", credentials.username);
|
||||
error!("Login {username} failed! {e}");
|
||||
web::Json(UserObj {
|
||||
message: format!("Login {} failed!", credentials.username),
|
||||
message: format!("Login {username} failed!"),
|
||||
user: None,
|
||||
})
|
||||
.customize()
|
||||
|
@ -9,8 +9,8 @@ use sqlx::{sqlite::SqliteQueryResult, Pool, Sqlite};
|
||||
use tokio::task;
|
||||
|
||||
use super::models::{AdvancedConfiguration, Configuration};
|
||||
use crate::db::models::{Channel, TextPreset, User};
|
||||
use crate::utils::{local_utc_offset, GlobalSettings, Role};
|
||||
use crate::db::models::{Channel, GlobalSettings, Role, TextPreset, User};
|
||||
use crate::utils::local_utc_offset;
|
||||
|
||||
pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std::error::Error>> {
|
||||
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> {
|
||||
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
|
||||
}
|
||||
|
@ -1,11 +1,53 @@
|
||||
use std::{error::Error, fmt, str::FromStr};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use regex::Regex;
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
|
||||
|
||||
use crate::db::handles;
|
||||
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)]
|
||||
pub struct User {
|
||||
#[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)]
|
||||
pub struct TextPreset {
|
||||
#[sqlx(default)]
|
||||
|
@ -21,12 +21,14 @@ use path_clean::PathClean;
|
||||
|
||||
use ffplayout::{
|
||||
api::{auth, routes::*},
|
||||
db::{db_pool, handles, models::LoginUser},
|
||||
db::{
|
||||
db_pool, handles,
|
||||
models::{init_globales, LoginUser},
|
||||
},
|
||||
player::controller::{ChannelController, ChannelManager},
|
||||
sse::{broadcast::Broadcaster, routes::*, AuthState},
|
||||
utils::{
|
||||
config::PlayoutConfig,
|
||||
init_globales,
|
||||
logging::{init_logging, MailQueue},
|
||||
run_args,
|
||||
},
|
||||
|
@ -5,9 +5,10 @@ use actix_web_grants::proc_macro::protect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{check_uuid, prune_uuids, AuthState, UuidData};
|
||||
use crate::db::models::Role;
|
||||
use crate::player::controller::ChannelController;
|
||||
use crate::sse::broadcast::Broadcaster;
|
||||
use crate::utils::{errors::ServiceError, Role};
|
||||
use crate::utils::errors::ServiceError;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct User {
|
||||
|
@ -10,12 +10,13 @@ pub struct Args {
|
||||
#[clap(short, long, help = "ask for user credentials")]
|
||||
pub ask: bool,
|
||||
|
||||
#[clap(long, help = "path to database file")]
|
||||
#[clap(long, env, help = "path to database file")]
|
||||
pub db: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
env,
|
||||
help = "Run channels by ids immediately (works without webserver and frontend, no listening parameter is needed)",
|
||||
num_args = 1..,
|
||||
)]
|
||||
@ -24,24 +25,37 @@ pub struct Args {
|
||||
#[clap(long, help = "List available channel ids")]
|
||||
pub list_channels: bool,
|
||||
|
||||
#[clap(long, help = "path to public files")]
|
||||
#[clap(long, env, help = "path to public files")]
|
||||
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>,
|
||||
|
||||
#[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>,
|
||||
|
||||
#[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>,
|
||||
|
||||
#[clap(long, help = "Logging path")]
|
||||
#[clap(long, env, help = "Logging path")]
|
||||
pub log_path: Option<PathBuf>,
|
||||
|
||||
#[clap(long, help = "Log to console")]
|
||||
#[clap(long, env, help = "Log to console")]
|
||||
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")]
|
||||
pub domain: Option<String>,
|
||||
|
||||
|
@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use shlex::split;
|
||||
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::vec_strings;
|
||||
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.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct PlayoutConfig {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub global: Global,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub advanced: AdvancedConfig,
|
||||
pub general: General,
|
||||
@ -165,6 +167,25 @@ pub struct PlayoutConfig {
|
||||
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)]
|
||||
pub struct General {
|
||||
pub help_text: String,
|
||||
@ -188,7 +209,7 @@ pub struct General {
|
||||
}
|
||||
|
||||
impl General {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.general_help.clone(),
|
||||
id: config.id,
|
||||
@ -218,7 +239,7 @@ pub struct Mail {
|
||||
}
|
||||
|
||||
impl Mail {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.mail_help.clone(),
|
||||
subject: config.subject.clone(),
|
||||
@ -259,7 +280,7 @@ pub struct Logging {
|
||||
}
|
||||
|
||||
impl Logging {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.logging_help.clone(),
|
||||
ffmpeg_level: config.ffmpeg_level.clone(),
|
||||
@ -301,7 +322,7 @@ pub struct Processing {
|
||||
}
|
||||
|
||||
impl Processing {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.processing_help.clone(),
|
||||
mode: ProcessMode::new(&config.processing_mode.clone()),
|
||||
@ -338,7 +359,7 @@ pub struct Ingest {
|
||||
}
|
||||
|
||||
impl Ingest {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.ingest_help.clone(),
|
||||
enable: config.ingest_enable,
|
||||
@ -363,7 +384,7 @@ pub struct Playlist {
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.playlist_help.clone(),
|
||||
path: PathBuf::from(config.playlist_path.clone()),
|
||||
@ -388,7 +409,7 @@ pub struct Storage {
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.storage_help.clone(),
|
||||
path: PathBuf::from(config.storage_path.clone()),
|
||||
@ -421,7 +442,7 @@ pub struct Text {
|
||||
}
|
||||
|
||||
impl Text {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.text_help.clone(),
|
||||
add_text: config.add_text,
|
||||
@ -444,7 +465,7 @@ pub struct Task {
|
||||
}
|
||||
|
||||
impl Task {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.task_help.clone(),
|
||||
enable: config.task_enable,
|
||||
@ -467,7 +488,7 @@ pub struct Output {
|
||||
}
|
||||
|
||||
impl Output {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
fn new(config: &models::Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.output_help.clone(),
|
||||
mode: OutputMode::new(&config.output_mode),
|
||||
@ -521,6 +542,9 @@ fn default_track_index() -> i32 {
|
||||
|
||||
impl PlayoutConfig {
|
||||
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)
|
||||
.await
|
||||
.expect("Can't read config");
|
||||
@ -528,6 +552,7 @@ impl PlayoutConfig {
|
||||
.await
|
||||
.expect("Can't read advanced config");
|
||||
|
||||
let global = Global::new(&global);
|
||||
let advanced = AdvancedConfig::new(adv_config);
|
||||
let general = General::new(&config);
|
||||
let mail = Mail::new(&config);
|
||||
@ -644,6 +669,7 @@ impl PlayoutConfig {
|
||||
}
|
||||
|
||||
Self {
|
||||
global,
|
||||
advanced,
|
||||
general,
|
||||
mail,
|
||||
|
@ -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 {
|
||||
fn from(err: actix_web::error::BlockingError) -> ServiceError {
|
||||
ServiceError::BadRequest(err.to_string())
|
||||
|
@ -1,18 +1,14 @@
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
fmt,
|
||||
env, fmt,
|
||||
fs::{self, metadata},
|
||||
io::{stdin, stdout, Write},
|
||||
net::TcpListener,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chrono::{format::ParseErrorKind, prelude::*};
|
||||
use faccess::PathExt;
|
||||
use log::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use path_clean::PathClean;
|
||||
use rand::Rng;
|
||||
use regex::Regex;
|
||||
@ -21,9 +17,6 @@ use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use sqlx::{sqlite::SqliteRow, FromRow, Pool, Row, Sqlite};
|
||||
|
||||
use crate::ARGS;
|
||||
|
||||
pub mod advanced_config;
|
||||
pub mod args_parse;
|
||||
@ -38,109 +31,10 @@ pub mod playlist;
|
||||
pub mod system;
|
||||
pub mod task_runner;
|
||||
|
||||
use crate::db::{
|
||||
db_pool,
|
||||
handles::{insert_user, select_global},
|
||||
models::User,
|
||||
};
|
||||
use crate::db::{db_pool, handles::insert_user, models::User};
|
||||
use crate::player::utils::time_to_sec;
|
||||
use crate::utils::{errors::ServiceError, logging::log_file_path};
|
||||
|
||||
#[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();
|
||||
}
|
||||
use crate::ARGS;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct TextFilter {
|
||||
|
@ -5,6 +5,10 @@ CREATE TABLE
|
||||
global (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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)
|
||||
);
|
||||
|
||||
@ -78,7 +82,7 @@ CREATE TABLE
|
||||
ingest_level TEXT NOT NULL DEFAULT "ERROR",
|
||||
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",
|
||||
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",
|
||||
audio_only 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,
|
||||
fps REAL NOT NULL DEFAULT 25.0,
|
||||
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_opacity REAL NOT NULL DEFAULT 0.7,
|
||||
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_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_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists",
|
||||
day_start TEXT NOT NULL DEFAULT "05:59:25",
|
||||
length TEXT NOT NULL DEFAULT "24:00:00",
|
||||
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_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media",
|
||||
filler TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media/filler/filler.mp4",
|
||||
filler TEXT NOT NULL DEFAULT "filler/filler.mp4",
|
||||
extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
|
||||
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,
|
||||
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",
|
||||
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.",
|
||||
@ -122,7 +124,7 @@ CREATE TABLE
|
||||
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_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
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user