get/update advanced config,

This commit is contained in:
jb-alvarado 2024-06-17 20:19:03 +02:00
parent 6de9bb9caa
commit 730439abd2
14 changed files with 762 additions and 145 deletions

1
.gitignore vendored
View File

@ -25,4 +25,5 @@ ffplayout.1.gz
/public/
tmp/
assets/playlist_template.json
advanced_*.toml
ffplayout_*.toml

66
Cargo.lock generated
View File

@ -679,7 +679,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
"stacker",
]
@ -907,7 +907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
@ -931,6 +931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
@ -1111,6 +1112,7 @@ dependencies = [
"sanitize-filename",
"serde",
"serde_json",
"serde_with",
"shlex",
"signal-child",
"sqlx",
@ -1373,13 +1375,19 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap",
"indexmap 2.2.6",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -1396,7 +1404,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@ -1747,6 +1755,17 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -1754,7 +1773,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.5",
"serde",
]
[[package]]
@ -2838,7 +2858,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5"
dependencies = [
"form_urlencoded",
"indexmap",
"indexmap 2.2.6",
"itoa",
"ryu",
"serde",
@ -2885,6 +2905,36 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.2.6",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "serial_test"
version = "3.1.1"
@ -3069,7 +3119,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap",
"indexmap 2.2.6",
"log",
"memchr",
"once_cell",
@ -3618,7 +3668,7 @@ version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",

View File

@ -51,6 +51,7 @@ rpassword = "7.2"
sanitize-filename = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = "3.8"
shlex = "1.1"
static-files = "0.2"
sysinfo ={ version = "0.30", features = ["linux-netdevs"] }

View File

@ -15,15 +15,17 @@ const JWT_EXPIRATION_DAYS: i64 = 7;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Claims {
pub id: i32,
pub channel: i32,
pub username: String,
pub role: Role,
exp: i64,
}
impl Claims {
pub fn new(id: i32, username: String, role: Role) -> Self {
pub fn new(id: i32, channel: i32, username: String, role: Role) -> Self {
Self {
id,
channel,
username,
role,
exp: (Utc::now() + TimeDelta::try_days(JWT_EXPIRATION_DAYS).unwrap()).timestamp(),

View File

@ -25,6 +25,7 @@ use actix_web::{
patch, post, put, web, HttpRequest, HttpResponse, Responder,
};
use actix_web_grants::{authorities::AuthDetails, proc_macro::protect};
use shlex::split;
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString},
@ -38,7 +39,6 @@ use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
use tokio::fs;
use crate::api::auth::{create_jwt, Claims};
use crate::db::models::Role;
use crate::utils::{
channels::{create_channel, delete_channel},
@ -54,10 +54,14 @@ use crate::utils::{
public_path, read_log_file, system, TextFilter,
};
use crate::vec_strings;
use crate::{
api::auth::{create_jwt, Claims},
utils::advanced_config::AdvancedConfig,
};
use crate::{
db::{
handles,
models::{Channel, LoginUser, TextPreset, User},
models::{Channel, TextPreset, User, UserMeta},
},
player::controller::ChannelController,
};
@ -180,7 +184,12 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
.await;
if verified_password.is_ok() {
let claims = Claims::new(user.id, username.clone(), role.clone());
let claims = Claims::new(
user.id,
user.channel_id.unwrap_or_default(),
username.clone(),
role.clone(),
);
if let Ok(token) = create_jwt(claims).await {
user.token = Some(token);
@ -227,12 +236,15 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/user")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role"
)]
async fn get_user(
pool: web::Data<Pool<Sqlite>>,
user: web::ReqData<LoginUser>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
match handles::select_user(&pool.into_inner(), &user.username).await {
match handles::select_user(&pool.into_inner(), user.id).await {
Ok(user) => Ok(web::Json(user)),
Err(e) => {
error!("{e}");
@ -247,13 +259,13 @@ async fn get_user(
/// curl -X GET 'http://127.0.0.1:8787/api/user/2' -H 'Content-Type: application/json' \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/user/{name}")]
#[get("/user/{id}")]
#[protect("Role::GlobalAdmin", ty = "Role")]
async fn get_by_name(
pool: web::Data<Pool<Sqlite>>,
name: web::Path<String>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
match handles::select_user(&pool.into_inner(), &name).await {
match handles::select_user(&pool.into_inner(), *id).await {
Ok(user) => Ok(web::Json(user)),
Err(e) => {
error!("{e}");
@ -287,49 +299,56 @@ async fn get_users(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, Serv
/// -d '{"mail": "<MAIL>", "password": "<PASS>"}' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[put("/user/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.id || role.has_authority(&Role::GlobalAdmin)"
)]
async fn update_user(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
user: web::ReqData<LoginUser>,
data: web::Json<User>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if *id == user.id || role.has_authority(&Role::GlobalAdmin) {
let mut fields = String::new();
let mut fields = String::new();
if let Some(mail) = data.mail.clone() {
if !fields.is_empty() {
fields.push_str(", ");
}
fields.push_str(format!("mail = '{mail}'").as_str());
if let Some(mail) = data.mail.clone() {
if !fields.is_empty() {
fields.push_str(", ");
}
if !data.password.is_empty() {
if !fields.is_empty() {
fields.push_str(", ");
}
let salt = SaltString::generate(&mut OsRng);
let password_hash = Argon2::default()
.hash_password(data.password.clone().as_bytes(), &salt)
.unwrap();
fields.push_str(format!("password = '{password_hash}'").as_str());
}
if handles::update_user(&pool.into_inner(), *id, fields)
.await
.is_ok()
{
return Ok("Update Success");
};
return Err(ServiceError::InternalServerError);
fields.push_str(format!("mail = '{mail}'").as_str());
}
Err(ServiceError::Unauthorized("No Permission".to_string()))
if !data.password.is_empty() {
if !fields.is_empty() {
fields.push_str(", ");
}
let password_hash = web::block(move || {
let salt = SaltString::generate(&mut OsRng);
let argon = Argon2::default()
.hash_password(data.password.clone().as_bytes(), &salt)
.map(|p| p.to_string());
argon
})
.await?
.unwrap();
fields.push_str(format!("password = '{password_hash}'").as_str());
}
if handles::update_user(&pool.into_inner(), *id, fields)
.await
.is_ok()
{
return Ok("Update Success");
};
Err(ServiceError::InternalServerError)
}
/// **Add User**
@ -360,13 +379,13 @@ async fn add_user(
/// curl -X GET 'http://127.0.0.1:8787/api/user/2' -H 'Content-Type: application/json' \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[delete("/user/{name}")]
#[delete("/user/{id}")]
#[protect("Role::GlobalAdmin", ty = "Role")]
async fn remove_user(
pool: web::Data<Pool<Sqlite>>,
name: web::Path<String>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
match handles::delete_user(&pool.into_inner(), &name).await {
match handles::delete_user(&pool.into_inner(), *id).await {
Ok(_) => return Ok("Delete user success"),
Err(e) => {
error!("{e}");
@ -395,10 +414,16 @@ async fn remove_user(
/// }
/// ```
#[get("/channel/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn get_channel(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_channel(&pool.into_inner(), &id).await {
return Ok(web::Json(channel));
@ -413,7 +438,7 @@ async fn get_channel(
/// curl -X GET http://127.0.0.1:8787/api/channels -H "Authorization: Bearer <TOKEN>"
/// ```
#[get("/channels")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(any("Role::GlobalAdmin"), ty = "Role")]
async fn get_all_channels(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_all_channels(&pool.into_inner()).await {
return Ok(web::Json(channel));
@ -430,11 +455,18 @@ async fn get_all_channels(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responde
/// -H "Authorization: Bearer <TOKEN>"
/// ```
#[patch("/channel/{id}")]
#[protect("Role::GlobalAdmin", ty = "Role")]
#[protect(
"Role::GlobalAdmin",
"Role::ChannelAdmin",
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn patch_channel(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
data: web::Json<Channel>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if handles::update_channel(&pool.into_inner(), *id, data.into_inner())
.await
@ -494,6 +526,151 @@ async fn remove_channel(
/// #### ffplayout Config
///
/// **Get Advanced Config**
///
/// ```BASH
/// curl -X GET http://127.0.0.1:8787/api/playout/advanced/1 -H 'Authorization: Bearer <TOKEN>'
/// ```
///
/// Response is a JSON object
#[get("/playout/advanced/{id}")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn get_advanced_config(
id: web::Path<i32>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().advanced.clone();
Ok(web::Json(config))
}
/// **Update Config**
///
/// ```BASH
/// curl -X PUT http://127.0.0.1:8787/api/playout/advanced/1 -H "Content-Type: application/json" \
/// -d { <CONFIG DATA> } -H 'Authorization: Bearer <TOKEN>'
/// ```
#[put("/playout/advanced/{id}")]
#[protect(
"Role::GlobalAdmin",
"Role::ChannelAdmin",
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn update_advanced_config(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
data: web::Json<AdvancedConfig>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let id = manager.config.lock().unwrap().general.id;
if let Err(e) =
handles::update_advanced_configuration(&pool.into_inner(), id, data.clone()).await
{
return Err(ServiceError::Conflict(format!("{e}")));
}
let mut cfg = manager.config.lock().unwrap();
cfg.advanced
.decoder
.input_param
.clone_from(&data.decoder.input_param);
cfg.advanced
.decoder
.output_param
.clone_from(&data.decoder.output_param);
cfg.advanced.decoder.input_cmd = split(&data.decoder.input_param.clone().unwrap_or_default());
cfg.advanced.decoder.output_cmd = split(&data.decoder.output_param.clone().unwrap_or_default());
cfg.advanced
.encoder
.input_param
.clone_from(&data.encoder.input_param);
cfg.advanced.encoder.input_cmd = split(&data.encoder.input_param.clone().unwrap_or_default());
cfg.advanced
.ingest
.input_param
.clone_from(&data.encoder.input_param);
cfg.advanced.ingest.input_cmd = split(&data.ingest.input_param.clone().unwrap_or_default());
cfg.advanced
.filter
.deinterlace
.clone_from(&data.filter.deinterlace);
cfg.advanced
.filter
.pad_scale_w
.clone_from(&data.filter.pad_scale_w);
cfg.advanced
.filter
.pad_scale_h
.clone_from(&data.filter.pad_scale_h);
cfg.advanced
.filter
.pad_video
.clone_from(&data.filter.pad_video);
cfg.advanced.filter.fps.clone_from(&data.filter.fps);
cfg.advanced.filter.scale.clone_from(&data.filter.scale);
cfg.advanced.filter.set_dar.clone_from(&data.filter.set_dar);
cfg.advanced.filter.fade_in.clone_from(&data.filter.fade_in);
cfg.advanced
.filter
.fade_out
.clone_from(&data.filter.fade_out);
cfg.advanced
.filter
.overlay_logo_scale
.clone_from(&data.filter.overlay_logo_scale);
cfg.advanced
.filter
.overlay_logo_fade_in
.clone_from(&data.filter.overlay_logo_fade_in);
cfg.advanced
.filter
.overlay_logo_fade_out
.clone_from(&data.filter.overlay_logo_fade_out);
cfg.advanced
.filter
.overlay_logo
.clone_from(&data.filter.overlay_logo);
cfg.advanced.filter.tpad.clone_from(&data.filter.tpad);
cfg.advanced
.filter
.drawtext_from_file
.clone_from(&data.filter.drawtext_from_file);
cfg.advanced
.filter
.drawtext_from_zmq
.clone_from(&data.filter.drawtext_from_zmq);
cfg.advanced
.filter
.aevalsrc
.clone_from(&data.filter.aevalsrc);
cfg.advanced
.filter
.afade_in
.clone_from(&data.filter.afade_in);
cfg.advanced
.filter
.afade_out
.clone_from(&data.filter.afade_out);
cfg.advanced.filter.apad.clone_from(&data.filter.apad);
cfg.advanced.filter.volume.clone_from(&data.filter.volume);
cfg.advanced.filter.split.clone_from(&data.filter.split);
Ok(web::Json("Update success"))
}
/// **Get Config**
///
/// ```BASH
@ -502,12 +679,16 @@ async fn remove_channel(
///
/// Response is a JSON object
#[get("/playout/config/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn get_playout_config(
_pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
_details: AuthDetails<Role>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -522,12 +703,19 @@ async fn get_playout_config(
/// -d { <CONFIG DATA> } -H 'Authorization: Bearer <TOKEN>'
/// ```
#[put("/playout/config/{id}")]
#[protect("Role::GlobalAdmin", ty = "Role")]
#[protect(
"Role::GlobalAdmin",
"Role::ChannelAdmin",
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn update_playout_config(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
data: web::Json<PlayoutConfig>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let id = manager.config.lock().unwrap().general.id;
@ -635,10 +823,16 @@ async fn update_playout_config(
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/presets/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn get_presets(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if let Ok(presets) = handles::select_presets(&pool.into_inner(), *id).await {
return Ok(web::Json(presets));
@ -655,11 +849,17 @@ async fn get_presets(
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[put("/presets/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn update_preset(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
data: web::Json<TextPreset>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if handles::update_preset(&pool.into_inner(), &id, data.into_inner())
.await
@ -674,15 +874,22 @@ async fn update_preset(
/// **Add new Preset**
///
/// ```BASH
/// curl -X POST http://127.0.0.1:8787/api/presets/ -H 'Content-Type: application/json' \
/// curl -X POST http://127.0.0.1:8787/api/presets/1/ -H 'Content-Type: application/json' \
/// -d '{ "name": "<PRESET NAME>", "text": "TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0, "channel_id": 1 }' \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/presets/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[post("/presets/{id}/")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn add_preset(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
data: web::Json<TextPreset>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if handles::insert_preset(&pool.into_inner(), data.into_inner())
.await
@ -701,10 +908,16 @@ async fn add_preset(
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[delete("/presets/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn delete_preset(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
if handles::delete_preset(&pool.into_inner(), &id)
.await
@ -732,11 +945,17 @@ async fn delete_preset(
/// -d '{"text": "Hello from ffplayout", "x": "(w-text_w)/2", "y": "(h-text_h)/2", fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", "box": "1", "boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0"}'
/// ```
#[post("/control/{id}/text/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn send_text_message(
id: web::Path<i32>,
data: web::Json<TextFilter>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
@ -757,12 +976,18 @@ pub async fn send_text_message(
/// -d '{ "command": "reset" }' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/control/{id}/playout/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn control_playout(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
control: web::Json<ControlParams>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
@ -797,10 +1022,16 @@ pub async fn control_playout(
/// }
/// ```
#[get("/control/{id}/media/current")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn media_current(
id: web::Path<i32>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let media_map = get_data_map(&manager);
@ -822,11 +1053,17 @@ pub async fn media_current(
/// -d '{"command": "start"}'
/// ```
#[post("/control/{id}/process/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn process_control(
id: web::Path<i32>,
proc: web::Json<Process>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
@ -863,11 +1100,17 @@ pub async fn process_control(
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/playlist/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn get_playlist(
id: web::Path<i32>,
obj: web::Query<DateObj>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -886,11 +1129,17 @@ pub async fn get_playlist(
/// --data "{<JSON playlist data>}"
/// ```
#[post("/playlist/{id}/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn save_playlist(
id: web::Path<i32>,
data: web::Json<JsonPlaylist>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -920,14 +1169,21 @@ pub async fn save_playlist(
/// {"start": "10:00:00", "duration": "14:00:00", "shuffle": false, "paths": ["path/3", "path/4"]}]}}'
/// ```
#[post("/playlist/{id}/generate/{date}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn gen_playlist(
params: web::Path<(i32, String)>,
id: web::Path<i32>,
date: web::Path<String>,
data: Option<web::Json<PathsObj>>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(params.0).unwrap();
manager.config.lock().unwrap().general.generate = Some(vec![params.1.clone()]);
let manager = controllers.lock().unwrap().get(*id).unwrap();
manager.config.lock().unwrap().general.generate = Some(vec![date.clone()]);
let storage_path = manager.config.lock().unwrap().global.storage_path.clone();
if let Some(obj) = data {
@ -965,15 +1221,22 @@ pub async fn gen_playlist(
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[delete("/playlist/{id}/{date}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn del_playlist(
params: web::Path<(i32, String)>,
id: web::Path<i32>,
date: web::Path<String>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(params.0).unwrap();
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
match delete_playlist(&config, &params.1).await {
match delete_playlist(&config, &date).await {
Ok(m) => Ok(web::Json(m)),
Err(e) => Err(e),
}
@ -988,10 +1251,16 @@ pub async fn del_playlist(
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/log/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn get_log(
id: web::Path<i32>,
log: web::Query<DateObj>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
read_log_file(&id, &log.date).await
}
@ -1005,11 +1274,17 @@ pub async fn get_log(
/// -d '{ "source": "/" }' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/file/{id}/browse/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn file_browser(
id: web::Path<i32>,
data: web::Json<PathObject>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let channel = manager.channel.lock().unwrap().clone();
@ -1028,11 +1303,17 @@ pub async fn file_browser(
/// -d '{"source": "<FOLDER PATH>"}' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/file/{id}/create-folder/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn add_dir(
id: web::Path<i32>,
data: web::Json<PathObject>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<HttpResponse, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -1047,11 +1328,17 @@ pub async fn add_dir(
/// -d '{"source": "<SOURCE>", "target": "<TARGET>"}' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/file/{id}/rename/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn move_rename(
id: web::Path<i32>,
data: web::Json<MoveObject>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -1069,11 +1356,17 @@ pub async fn move_rename(
/// -d '{"source": "<SOURCE>"}' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/file/{id}/remove/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn remove(
id: web::Path<i32>,
data: web::Json<PathObject>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -1090,14 +1383,21 @@ pub async fn remove(
/// curl -X PUT http://127.0.0.1:8787/api/file/1/upload/ -H 'Authorization: Bearer <TOKEN>'
/// -F "file=@file.mp4"
/// ```
#[allow(clippy::too_many_arguments)]
#[put("/file/{id}/upload/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn save_file(
id: web::Path<i32>,
req: HttpRequest,
payload: Multipart,
obj: web::Query<FileObj>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<HttpResponse, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -1178,14 +1478,21 @@ async fn get_public(public: web::Path<String>) -> Result<actix_files::NamedFile,
/// curl -X PUT http://127.0.0.1:8787/api/file/1/import/ -H 'Authorization: Bearer <TOKEN>'
/// -F "file=@list.m3u"
/// ```
#[allow(clippy::too_many_arguments)]
#[put("/file/{id}/import/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn import_playlist(
id: web::Path<i32>,
req: HttpRequest,
payload: Multipart,
obj: web::Query<ImportObj>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<HttpResponse, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let channel_name = manager.channel.lock().unwrap().name.clone();
@ -1234,11 +1541,17 @@ async fn import_playlist(
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/program/{id}/")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
async fn get_program(
id: web::Path<i32>,
obj: web::Query<ProgramObj>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();
@ -1326,10 +1639,16 @@ async fn get_program(
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/system/{id}")]
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
#[protect(
any("Role::GlobalAdmin", "Role::ChannelAdmin", "Role::User"),
ty = "Role",
expr = "*id == user.channel || role.has_authority(&Role::GlobalAdmin)"
)]
pub async fn get_system_stat(
id: web::Path<i32>,
controllers: web::Data<Mutex<ChannelController>>,
role: AuthDetails<Role>,
user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(*id).unwrap();
let config = manager.config.lock().unwrap().clone();

View File

@ -167,15 +167,12 @@ pub async fn select_configuration(
pub async fn insert_configuration(
conn: &Pool<Sqlite>,
channel_id: i32,
playlist_path: String,
output_param: String,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query =
"INSERT INTO configurations (channel_id, playlist_path, output_param) VALUES($1, $2, $3)";
let query = "INSERT INTO configurations (channel_id, output_param) VALUES($1, $2)";
sqlx::query(query)
.bind(channel_id)
.bind(playlist_path)
.bind(output_param)
.execute(conn)
.await
@ -308,19 +305,14 @@ pub async fn select_role(conn: &Pool<Sqlite>, id: &i32) -> Result<Role, sqlx::Er
}
pub async fn select_login(conn: &Pool<Sqlite>, user: &str) -> Result<User, sqlx::Error> {
let query = "SELECT id, mail, username, password, role_id FROM user WHERE username = $1";
let query =
"SELECT id, mail, username, password, role_id, channel_id FROM user WHERE username = $1";
sqlx::query_as(query).bind(user).fetch_one(conn).await
}
pub async fn select_user(conn: &Pool<Sqlite>, user: &str) -> Result<User, sqlx::Error> {
let query = "SELECT id, mail, username, role_id FROM user WHERE username = $1";
sqlx::query_as(query).bind(user).fetch_one(conn).await
}
pub async fn select_user_by_id(conn: &Pool<Sqlite>, id: i32) -> Result<User, sqlx::Error> {
let query = "SELECT id, mail, username, role_id FROM user WHERE id = $1";
pub async fn select_user(conn: &Pool<Sqlite>, id: i32) -> Result<User, sqlx::Error> {
let query = "SELECT id, mail, username, role_id, channel_id FROM user WHERE id = $1";
sqlx::query_as(query).bind(id).fetch_one(conn).await
}
@ -367,13 +359,10 @@ pub async fn update_user(
sqlx::query(&query).bind(id).execute(conn).await
}
pub async fn delete_user(
conn: &Pool<Sqlite>,
name: &str,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "DELETE FROM user WHERE username = $1;";
pub async fn delete_user(conn: &Pool<Sqlite>, id: i32) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "DELETE FROM user WHERE id = $1;";
sqlx::query(query).bind(name).execute(conn).await
sqlx::query(query).bind(id).execute(conn).await
}
pub async fn select_presets(conn: &Pool<Sqlite>, id: i32) -> Result<Vec<TextPreset>, sqlx::Error> {

View File

@ -80,14 +80,14 @@ fn empty_string() -> String {
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LoginUser {
pub struct UserMeta {
pub id: i32,
pub username: String,
pub channel: i32,
}
impl LoginUser {
pub fn new(id: i32, username: String) -> Self {
Self { id, username }
impl UserMeta {
pub fn new(id: i32, channel: i32) -> Self {
Self { id, channel }
}
}

View File

@ -23,7 +23,7 @@ use ffplayout::{
api::{auth, routes::*},
db::{
db_pool, handles,
models::{init_globales, LoginUser},
models::{init_globales, UserMeta},
},
player::controller::{ChannelController, ChannelManager},
sse::{broadcast::Broadcaster, routes::*, AuthState},
@ -59,7 +59,7 @@ async fn validator(
req.attach(vec![claims.role]);
req.extensions_mut()
.insert(LoginUser::new(claims.id, claims.username));
.insert(UserMeta::new(claims.id, claims.channel));
Ok(req)
}
@ -150,6 +150,8 @@ async fn main() -> std::io::Result<()> {
.service(get_by_name)
.service(get_users)
.service(remove_user)
.service(get_advanced_config)
.service(update_advanced_config)
.service(get_playout_config)
.service(update_playout_config)
.service(add_preset)
@ -225,15 +227,6 @@ async fn main() -> std::io::Result<()> {
.workers(thread_count)
.run()
.await
} else if ARGS.list_channels {
let channels = handles::select_all_channels(&pool)
.await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let ids = channels.iter().map(|c| c.id).collect::<Vec<i32>>();
info!("Available channels: {ids:?}");
Ok(())
} else if let Some(channels) = &ARGS.channels {
for (index, channel_id) in channels.iter().enumerate() {
let channel = handles::select_channel(&pool, channel_id).await.unwrap();

View File

@ -1,7 +1,13 @@
use serde::{Deserialize, Serialize};
use shlex::split;
use std::path::Path;
use crate::db::models::AdvancedConfiguration;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, NoneAsEmptyString};
use shlex::split;
use sqlx::{Pool, Sqlite};
use tokio::io::AsyncReadExt;
use crate::db::{handles, models::AdvancedConfiguration};
use crate::utils::ServiceError;
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct AdvancedConfig {
@ -11,49 +17,83 @@ pub struct AdvancedConfig {
pub ingest: IngestConfig,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct DecoderConfig {
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub output_param: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
#[serde(skip_serializing, skip_deserializing)]
pub output_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct EncoderConfig {
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct IngestConfig {
#[serde_as(as = "NoneAsEmptyString")]
pub input_param: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub input_cmd: Option<Vec<String>>,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct FilterConfig {
#[serde_as(as = "NoneAsEmptyString")]
pub deinterlace: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub pad_scale_w: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub pad_scale_h: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub pad_video: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub fps: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub scale: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub set_dar: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub fade_in: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub fade_out: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_scale: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_fade_in: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo_fade_out: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub overlay_logo: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub tpad: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub drawtext_from_file: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub drawtext_from_zmq: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub aevalsrc: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub afade_in: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub afade_out: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub apad: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub volume: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub split: Option<String>,
}
@ -112,4 +152,122 @@ impl AdvancedConfig {
},
}
}
pub async fn dump(pool: &Pool<Sqlite>, id: i32) -> Result<(), ServiceError> {
let config = Self::new(handles::select_advanced_configuration(pool, id).await?);
let f_keys = [
"deinterlace",
"pad_scale_w",
"pad_scale_h",
"pad_video",
"fps",
"scale",
"set_dar",
"fade_in",
"fade_out",
"overlay_logo_scale",
"overlay_logo_fade_in",
"overlay_logo_fade_out",
"overlay_logo",
"tpad",
"drawtext_from_file",
"drawtext_from_zmq",
"aevalsrc",
"afade_in",
"afade_out",
"apad",
"volume",
"split",
];
let toml_string = toml_edit::ser::to_string_pretty(&config)?;
let mut doc = toml_string.parse::<toml_edit::DocumentMut>()?;
if let Some(decoder) = doc.get_mut("decoder").and_then(|o| o.as_table_mut()) {
decoder
.decor_mut()
.set_prefix("# Changing these settings is for advanced users only!\n# There will be no support or guarantee that it will be stable after changing them.\n\n");
}
if let Some(output_param) = doc
.get_mut("decoder")
.and_then(|d| d.get_mut("output_param"))
.and_then(|o| o.as_value_mut())
{
output_param
.decor_mut()
.set_suffix(" # get also applied to ingest instance.");
}
if let Some(filter) = doc.get_mut("filter") {
for key in &f_keys {
if let Some(item) = filter.get_mut(*key).and_then(|o| o.as_value_mut()) {
match *key {
"deinterlace" => item.decor_mut().set_suffix(" # yadif=0:-1:0"),
"pad_scale_w" => item.decor_mut().set_suffix(" # scale={}:-1"),
"pad_scale_h" => item.decor_mut().set_suffix(" # scale=-1:{}"),
"pad_video" => item.decor_mut().set_suffix(
" # pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2",
),
"fps" => item.decor_mut().set_suffix(" # fps={}"),
"scale" => item.decor_mut().set_suffix(" # scale={}:{}"),
"set_dar" => item.decor_mut().set_suffix(" # setdar=dar={}"),
"fade_in" => item.decor_mut().set_suffix(" # fade=in:st=0:d=0.5"),
"fade_out" => item.decor_mut().set_suffix(" # fade=out:st={}:d=1.0"),
"overlay_logo_scale" => item.decor_mut().set_suffix(" # scale={}"),
"overlay_logo_fade_in" => {
item.decor_mut().set_suffix(" # fade=in:st=0:d=1.0:alpha=1")
}
"overlay_logo_fade_out" => item
.decor_mut()
.set_suffix(" # fade=out:st={}:d=1.0:alpha=1"),
"overlay_logo" => item
.decor_mut()
.set_suffix(" # null[l];[v][l]overlay={}:shortest=1"),
"tpad" => item
.decor_mut()
.set_suffix(" # tpad=stop_mode=add:stop_duration={}"),
"drawtext_from_file" => {
item.decor_mut().set_suffix(" # drawtext=text='{}':{}{}")
}
"drawtext_from_zmq" => item
.decor_mut()
.set_suffix(" # zmq=b=tcp\\\\://'{}',drawtext@dyntext={}"),
"aevalsrc" => item.decor_mut().set_suffix(
" # aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000",
),
"afade_in" => item.decor_mut().set_suffix(" # afade=in:st=0:d=0.5"),
"afade_out" => item.decor_mut().set_suffix(" # afade=out:st={}:d=1.0"),
"apad" => item.decor_mut().set_suffix(" # apad=whole_dur={}"),
"volume" => item.decor_mut().set_suffix(" # volume={}"),
"split" => item.decor_mut().set_suffix(" # split={}{}"),
_ => (),
}
}
}
};
tokio::fs::write(&format!("advanced_{id}.toml"), doc.to_string()).await?;
Ok(())
}
pub async fn import(pool: &Pool<Sqlite>, import: Vec<String>) -> Result<(), ServiceError> {
let id = import[0].parse::<i32>()?;
let path = Path::new(&import[1]);
if path.is_file() {
let mut file = tokio::fs::File::open(path).await?;
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
let config: Self = toml_edit::de::from_str(&contents).unwrap();
handles::update_advanced_configuration(pool, id, config).await?;
} else {
return Err(ServiceError::BadRequest("Path not exists!".to_string()));
}
Ok(())
}
}

View File

@ -12,7 +12,7 @@ use crate::db::{
handles::{self, insert_user},
models::{GlobalSettings, User},
};
use crate::utils::config::PlayoutConfig;
use crate::utils::{advanced_config::AdvancedConfig, config::PlayoutConfig};
use crate::ARGS;
#[derive(Parser, Debug, Clone)]
@ -39,9 +39,22 @@ pub struct Args {
)]
pub channels: Option<Vec<i32>>,
#[clap(
long,
help = "Dump advanced channel configuration to advanced_{channel}.toml"
)]
pub dump_advanced: Option<i32>,
#[clap(long, help = "Dump channel configuration to ffplayout_{channel}.toml")]
pub dump_config: Option<i32>,
#[clap(
long,
help = "import advanced channel configuration from file. Input must be `{channel id} {path to toml}`",
num_args = 2
)]
pub import_advanced: Option<Vec<String>>,
#[clap(
long,
help = "import channel configuration from file. Input must be `{channel id} {path to toml}`",
@ -251,6 +264,32 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
return Err(0);
}
if ARGS.list_channels {
match handles::select_all_channels(pool).await {
Ok(channels) => {
let chl = channels
.iter()
.map(|c| (c.id, c.name.clone()))
.collect::<Vec<(i32, String)>>();
println!(
"Available channels:\n{}",
chl.iter()
.map(|(i, t)| format!(" {i}: '{t}'"))
.collect::<Vec<String>>()
.join("\n")
);
return Err(0);
}
Err(e) => {
eprintln!("List channels: {e}");
exit(1);
}
}
}
if let Some(id) = ARGS.dump_config {
match PlayoutConfig::dump(pool, id).await {
Ok(_) => {
@ -265,6 +304,34 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
};
}
if let Some(id) = ARGS.dump_config {
match PlayoutConfig::dump(pool, id).await {
Ok(_) => {
println!("Dump config to: ffplayout_{id}.toml");
exit(0);
}
Err(e) => {
eprintln!("Dump config: {e}");
exit(1);
}
};
}
if let Some(id) = ARGS.dump_advanced {
match AdvancedConfig::dump(pool, id).await {
Ok(_) => {
println!("Dump config to: advanced_{id}.toml");
exit(0);
}
Err(e) => {
eprintln!("Dump config: {e}");
exit(1);
}
};
}
if let Some(import) = &ARGS.import_config {
match PlayoutConfig::import(pool, import.clone()).await {
Ok(_) => {
@ -279,5 +346,19 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
};
}
if let Some(import) = &ARGS.import_advanced {
match AdvancedConfig::import(pool, import.clone()).await {
Ok(_) => {
println!("Import config done...");
exit(0);
}
Err(e) => {
eprintln!("{e}");
exit(1);
}
};
}
Ok(())
}

View File

@ -16,14 +16,12 @@ pub async fn create_channel(
queue: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>,
target_channel: Channel,
) -> Result<Channel, ServiceError> {
let channel_name = target_channel.name.to_lowercase().replace(' ', "");
let channel = handles::insert_channel(conn, target_channel).await?;
let playlist_path = format!("/var/lib/ffplayout/playlists/{channel_name}");
let output_param = format!("-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{0}-%d.ts /usr/share/ffplayout/public/live/stream{0}.m3u8", channel.id);
handles::insert_advanced_configuration(conn, channel.id).await?;
handles::insert_configuration(conn, channel.id, playlist_path, output_param).await?;
handles::insert_configuration(conn, channel.id, output_param).await?;
let config = PlayoutConfig::new(conn, channel.id).await;
let m_queue = Arc::new(Mutex::new(MailQueue::new(channel.id, config.mail.clone())));

View File

@ -542,14 +542,14 @@ fn default_track_index() -> i32 {
// }
impl PlayoutConfig {
pub async fn new(pool: &Pool<Sqlite>, channel: i32) -> Self {
pub async fn new(pool: &Pool<Sqlite>, channel_id: 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_id)
.await
.expect("Can't read config");
let adv_config = handles::select_advanced_configuration(pool, channel)
let adv_config = handles::select_advanced_configuration(pool, channel_id)
.await
.expect("Can't read advanced config");
@ -567,7 +567,23 @@ impl PlayoutConfig {
let mut output = Output::new(&config);
if !global.shared_storage {
global.storage_path = global.storage_path.join(channel.to_string());
global.storage_path = global.storage_path.join(channel_id.to_string());
}
if !global.storage_path.is_dir() {
tokio::fs::create_dir_all(&global.storage_path)
.await
.expect("Can't create storage folder");
}
if channel_id > 1 || !global.shared_storage {
global.playlist_path = global.playlist_path.join(channel_id.to_string());
}
if !global.playlist_path.is_dir() {
tokio::fs::create_dir_all(&global.playlist_path)
.await
.expect("Can't create playlist folder");
}
let (filler_path, _, _) = norm_abs_path(&global.storage_path, &config.storage_filler)
@ -696,12 +712,14 @@ impl PlayoutConfig {
pub async fn dump(pool: &Pool<Sqlite>, id: i32) -> Result<(), ServiceError> {
let mut config = Self::new(pool, id).await;
config.storage.filler = config
.storage
.filler
.strip_prefix(config.global.storage_path.clone())
.unwrap_or(&config.storage.filler)
.to_owned();
config.storage.filler.clone_from(
&config
.storage
.filler
.strip_prefix(config.global.storage_path.clone())
.unwrap_or(&config.storage.filler)
.to_path_buf(),
);
let toml_string = toml_edit::ser::to_string_pretty(&config)?;
tokio::fs::write(&format!("ffplayout_{id}.toml"), toml_string).await?;

View File

@ -107,6 +107,12 @@ impl From<toml_edit::ser::Error> for ServiceError {
}
}
impl From<toml_edit::TomlError> for ServiceError {
fn from(err: toml_edit::TomlError) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}
impl From<uuid::Error> for ServiceError {
fn from(err: uuid::Error) -> ServiceError {
ServiceError::BadRequest(err.to_string())

View File

@ -13,9 +13,9 @@ pub async fn read_playlist(
config: &PlayoutConfig,
date: String,
) -> Result<JsonPlaylist, ServiceError> {
let (path, _, _) = norm_abs_path(&config.global.playlist_path, "")?;
let mut playlist_path = path;
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.global.playlist_path.clone();
playlist_path = playlist_path
.join(d[0])
.join(d[1])
@ -33,8 +33,8 @@ pub async fn write_playlist(
json_data: JsonPlaylist,
) -> Result<String, ServiceError> {
let date = json_data.date.clone();
let mut playlist_path = config.global.playlist_path.clone();
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.global.playlist_path.clone();
if !playlist_path
.extension()
@ -123,8 +123,9 @@ pub fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, Servic
}
pub async fn delete_playlist(config: &PlayoutConfig, date: &str) -> Result<String, ServiceError> {
let mut playlist_path = PathBuf::from(&config.global.playlist_path);
let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = PathBuf::from(&config.global.playlist_path);
playlist_path = playlist_path
.join(d[0])
.join(d[1])