/// ### Possible endpoints /// /// Run the API thru the systemd service, or like: /// /// ```BASH /// ffpapi -l 127.0.0.1:8000 /// ``` /// /// For all endpoints an (Bearer) authentication is required.\ /// `{id}` represent the channel id, and at default is 1. use std::collections::HashMap; use actix_multipart::Multipart; use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder}; use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, Argon2, PasswordHasher, PasswordVerifier, }; use serde::{Deserialize, Serialize}; use simplelog::*; use crate::utils::{ auth::{create_jwt, Claims}, control::{control_service, control_state, media_info, send_message, Process}, errors::ServiceError, files::{ browser, create_directory, remove_file_or_folder, rename_file, upload, MoveObject, PathObject, }, handles::{ db_add_preset, db_add_user, db_get_all_settings, db_get_presets, db_get_settings, db_get_user, db_login, db_role, db_update_preset, db_update_settings, db_update_user, db_delete_preset, }, models::{LoginUser, Settings, TextPreset, User}, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, read_log_file, read_playout_config, Role, }; use ffplayout_lib::utils::{JsonPlaylist, PlayoutConfig}; #[derive(Serialize)] struct ResponseObj { message: String, status: i32, data: Option, } #[derive(Serialize)] struct UserObj { message: String, user: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct DateObj { #[serde(default)] date: String, } #[derive(Debug, Deserialize, Serialize)] pub struct FileObj { #[serde(default)] path: String, } /// #### User Handling /// /// **Login** /// /// ```BASH /// curl -X POST http://127.0.0.1:8000/auth/login/ -H "Content-Type: application/json" \ /// -d '{ "username": "", "password": "" }' /// ``` /// **Response:** /// /// ```JSON /// { /// "id": 1, /// "mail": "user@example.org", /// "username": "", /// "token": "" /// } /// ``` #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { match db_login(&credentials.username).await { Ok(mut user) => { let pass = user.password.clone(); let hash = PasswordHash::new(&pass).unwrap(); user.password = "".into(); user.salt = None; if Argon2::default() .verify_password(credentials.password.as_bytes(), &hash) .is_ok() { let role = db_role(&user.role_id.unwrap_or_default()) .await .unwrap_or_else(|_| "guest".to_string()); 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) } } Err(e) => { error!("Login {} failed! {e}", credentials.username); return web::Json(UserObj { message: format!("Login {} failed!", credentials.username), user: None, }) .customize() .with_status(StatusCode::BAD_REQUEST); } } } /// From here on all request **must** contain the authorization header:\ /// `"Authorization: Bearer "` /// **Get current User** /// /// ```BASH /// curl -X GET 'http://localhost:8000/api/user' -H 'Content-Type: application/json' \ /// -H 'Authorization: Bearer ' /// ``` #[get("/user")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_user(user: web::ReqData) -> Result { match db_get_user(&user.username).await { Ok(user) => Ok(web::Json(user)), Err(e) => { error!("{e}"); Err(ServiceError::InternalServerError) } } } /// **Update current User** /// /// ```BASH /// curl -X PUT http://localhost:8000/api/user/1 -H 'Content-Type: application/json' \ /// -d '{"mail": "", "password": ""}' -H 'Authorization: ' /// ``` #[put("/user/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn update_user( id: web::Path, user: web::ReqData, data: web::Json, ) -> Result { if id.into_inner() == user.id { let mut fields = String::new(); if let Some(mail) = data.mail.clone() { fields.push_str(format!("mail = '{mail}'").as_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 = '{}', salt = '{salt}'", password_hash).as_str()); } if db_update_user(user.id, fields).await.is_ok() { return Ok("Update Success"); }; return Err(ServiceError::InternalServerError); } Err(ServiceError::Unauthorized) } /// **Add User** /// /// ```BASH /// curl -X POST 'http://localhost:8000/api/user/' -H 'Content-Type: application/json' \ /// -d '{"mail": "", "username": "", "password": "", "role_id": 1, "channel_id": 1}' \ /// -H 'Authorization: Bearer ' /// ``` #[post("/user/")] #[has_any_role("Role::Admin", type = "Role")] async fn add_user(data: web::Json) -> Result { match db_add_user(data.into_inner()).await { Ok(_) => Ok("Add User Success"), Err(e) => { error!("{e}"); Err(ServiceError::InternalServerError) } } } /// #### ffpapi Settings /// /// **Get Settings** /// /// ```BASH /// curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer " /// ``` /// /// **Response:** /// /// ```JSON /// { /// "id": 1, /// "channel_name": "Channel 1", /// "preview_url": "http://localhost/live/preview.m3u8", /// "config_path": "/etc/ffplayout/ffplayout.yml", /// "extra_extensions": "jpg,jpeg,png", /// "timezone": "UTC", /// "service": "ffplayout.service" /// } /// ``` #[get("/settings/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_settings(id: web::Path) -> Result { if let Ok(settings) = db_get_settings(&id).await { return Ok(web::Json(settings)); } Err(ServiceError::InternalServerError) } /// **Get all Settings** /// /// ```BASH /// curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer " /// ``` #[get("/settings")] #[has_any_role("Role::Admin", type = "Role")] async fn get_all_settings() -> Result { if let Ok(settings) = db_get_all_settings().await { return Ok(web::Json(settings)); } Err(ServiceError::InternalServerError) } /// **Update Settings** /// /// ```BASH /// curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \ /// -d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \ /// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", /// "role_id": 1, "channel_id": 1 }' \ /// -H "Authorization: Bearer " /// ``` #[patch("/settings/{id}")] #[has_any_role("Role::Admin", type = "Role")] async fn patch_settings( id: web::Path, data: web::Json, ) -> Result { if db_update_settings(*id, data.into_inner()).await.is_ok() { return Ok("Update Success"); }; Err(ServiceError::InternalServerError) } /// #### ffplayout Config /// /// **Get Config** /// /// ```BASH /// curl -X GET http://localhost:8000/api/playout/config/1 -H 'Authorization: ' /// ``` /// /// Response is a JSON object from the ffplayout.yml #[get("/playout/config/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_playout_config( id: web::Path, _details: AuthDetails, ) -> Result { if let Ok(settings) = db_get_settings(&id).await { if let Ok(config) = read_playout_config(&settings.config_path) { return Ok(web::Json(config)); } }; Err(ServiceError::InternalServerError) } /// **Update Config** /// /// ```BASH /// curl -X PUT http://localhost:8000/api/playout/config/1 -H "Content-Type: application/json" \ /// -d { } -H 'Authorization: ' /// ``` #[put("/playout/config/{id}")] #[has_any_role("Role::Admin", type = "Role")] async fn update_playout_config( id: web::Path, data: web::Json, ) -> Result { if let Ok(settings) = db_get_settings(&id).await { if let Ok(f) = std::fs::OpenOptions::new() .write(true) .truncate(true) .open(&settings.config_path) { serde_yaml::to_writer(f, &data).unwrap(); return Ok("Update playout config success."); } else { return Err(ServiceError::InternalServerError); }; }; Err(ServiceError::InternalServerError) } /// #### Text Presets /// /// Text presets are made for sending text messages to the ffplayout engine, to overlay them as a lower third. /// /// **Get all Presets** /// /// ```BASH /// curl -X GET http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \ /// -H 'Authorization: ' /// ``` #[get("/presets/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_presets(id: web::Path) -> Result { if let Ok(presets) = db_get_presets(*id).await { return Ok(web::Json(presets)); } Err(ServiceError::InternalServerError) } /// **Update Preset** /// /// ```BASH /// curl -X PUT http://localhost:8000/api/presets/1 -H 'Content-Type: application/json' \ /// -d '{ "name": "", "text": "", "x": "", "y": "", "fontsize": 24, \ /// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0, "channel_id": 1 }' \ /// -H 'Authorization: ' /// ``` #[put("/presets/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn update_preset( id: web::Path, data: web::Json, ) -> Result { if db_update_preset(&id, data.into_inner()).await.is_ok() { return Ok("Update Success"); } Err(ServiceError::InternalServerError) } /// **Add new Preset** /// /// ```BASH /// curl -X POST http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \ /// -d '{ "name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ /// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0, "channel_id": 1 }' \ /// -H 'Authorization: ' /// ``` #[post("/presets/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn add_preset(data: web::Json) -> Result { if db_add_preset(data.into_inner()).await.is_ok() { return Ok("Add preset Success"); } Err(ServiceError::InternalServerError) } /// **Delete Preset** /// /// ```BASH /// curl -X DELETE http://localhost:8000/api/presets/1 -H 'Content-Type: application/json' \ /// -H 'Authorization: ' /// ``` #[delete("/presets/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn delete_preset(id: web::Path) -> Result { if db_delete_preset(&id).await.is_ok() { return Ok("Delete preset Success"); } Err(ServiceError::InternalServerError) } /// ### ffplayout controlling /// /// here we communicate with the engine for: /// - jump to last or next clip /// - reset playlist state /// - get infos about current, next, last clip /// - send text to the engine, for overlaying it (as lower third etc.) /// /// **Send Text to ffplayout** /// /// ```BASH /// curl -X POST http://localhost:8000/api/control/1/text/ \ /// -H 'Content-Type: application/json' -H 'Authorization: ' \ /// -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/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn send_text_message( id: web::Path, data: web::Json>, ) -> Result { match send_message(*id, data.into_inner()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } /// **Control Playout** /// /// - next /// - back /// - reset /// /// ```BASH /// curl -X POST http://localhost:8000/api/control/1/playout/next/ -H 'Content-Type: application/json' /// -d '{ "command": "reset" }' -H 'Authorization: ' /// ``` #[post("/control/{id}/playout/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn control_playout(id: web::Path, control: web::Json) -> Result { match control_state(*id, control.command.clone()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } /// **Get current Clip** /// /// ```BASH /// curl -X GET http://localhost:8000/api/control/1/media/current /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` /// /// **Response:** /// /// ```JSON /// { /// "jsonrpc": "2.0", /// "result": { /// "current_media": { /// "category": "", /// "duration": 154.2, /// "out": 154.2, /// "seek": 0.0, /// "source": "/opt/tv-media/clip.mp4" /// }, /// "index": 39, /// "play_mode": "playlist", /// "played_sec": 67.80771999300123, /// "remaining_sec": 86.39228000699876, /// "start_sec": 24713.631999999998, /// "start_time": "06:51:53.631" /// }, /// "id": 1 /// } /// ``` #[get("/control/{id}/media/current")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_current(id: web::Path) -> Result { match media_info(*id, "current".into()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } /// **Get next Clip** /// /// ```BASH /// curl -X GET http://localhost:8000/api/control/1/media/next/ -H 'Authorization: ' /// ``` #[get("/control/{id}/media/next")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_next(id: web::Path) -> Result { match media_info(*id, "next".into()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } /// **Get last Clip** /// /// ```BASH /// curl -X GET http://localhost:8000/api/control/1/media/last/ /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` #[get("/control/{id}/media/last")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_last(id: web::Path) -> Result { match media_info(*id, "last".into()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } /// #### ffplayout Process Control /// /// Control ffplayout process, like: /// - start /// - stop /// - restart /// - status /// /// ```BASH /// curl -X POST http://localhost:8000/api/control/1/process/ /// -H 'Content-Type: application/json' -H 'Authorization: ' /// -d '{"command": "start"}' /// ``` #[post("/control/{id}/process/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn process_control( id: web::Path, proc: web::Json, ) -> Result { control_service(*id, &proc.command).await } /// #### ffplayout Playlist Operations /// /// **Get playlist** /// /// ```BASH /// curl -X GET http://localhost:8000/api/playlist/1?date=2022-06-20 /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` #[get("/playlist/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn get_playlist( id: web::Path, obj: web::Query, ) -> Result { match read_playlist(*id, obj.date.clone()).await { Ok(playlist) => Ok(web::Json(playlist)), Err(e) => Err(e), } } /// **Save playlist** /// /// ```BASH /// curl -X POST http://localhost:8000/api/playlist/1/ /// -H 'Content-Type: application/json' -H 'Authorization: ' /// -- data "{}" /// ``` #[post("/playlist/{id}/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn save_playlist( id: web::Path, data: web::Json, ) -> Result { match write_playlist(*id, data.into_inner()).await { Ok(res) => Ok(res), Err(e) => Err(e), } } /// **Generate Playlist** /// /// A new playlist will be generated and response. /// /// ```BASH /// curl -X GET http://localhost:8000/api/playlist/1/generate/2022-06-20 /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` #[get("/playlist/{id}/generate/{date}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn gen_playlist( params: web::Path<(i64, String)>, ) -> Result { match generate_playlist(params.0, params.1.clone()).await { Ok(playlist) => Ok(web::Json(playlist)), Err(e) => Err(e), } } /// **Delete Playlist** /// /// ```BASH /// curl -X DELETE http://localhost:8000/api/playlist/1/2022-06-20 /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` #[delete("/playlist/{id}/{date}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn del_playlist( params: web::Path<(i64, String)>, ) -> Result { match delete_playlist(params.0, ¶ms.1).await { Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)), Err(e) => Err(e), } } /// ### Log file /// /// **Read Log Life** /// /// ```BASH /// curl -X Get http://localhost:8000/api/log/1 /// -H 'Content-Type: application/json' -H 'Authorization: ' /// ``` #[get("/log/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn get_log( id: web::Path, log: web::Query, ) -> Result { read_log_file(&id, &log.date).await } /// ### File Operations /// /// **Get File/Folder List** /// /// ```BASH /// curl -X POST http://localhost:8000/api/file/1/browse/ -H 'Content-Type: application/json' /// -d '{ "source": "/" }' -H 'Authorization: ' /// ``` #[post("/file/{id}/browse/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn file_browser( id: web::Path, data: web::Json, ) -> Result { match browser(*id, &data.into_inner()).await { Ok(obj) => Ok(web::Json(obj)), Err(e) => Err(e), } } /// **Create Folder** /// /// ```BASH /// curl -X POST http://localhost:8000/api/file/1/create-folder/ -H 'Content-Type: application/json' /// -d '{"source": ""}' -H 'Authorization: ' /// ``` #[post("/file/{id}/create-folder/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn add_dir( id: web::Path, data: web::Json, ) -> Result { create_directory(*id, &data.into_inner()).await } /// **Rename File** /// /// ```BASH /// curl -X POST http://localhost:8000/api/file/1/rename/ -H 'Content-Type: application/json' /// -d '{"source": "", "target": ""}' -H 'Authorization: ' /// ``` #[post("/file/{id}/rename/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn move_rename( id: web::Path, data: web::Json, ) -> Result { match rename_file(*id, &data.into_inner()).await { Ok(obj) => Ok(web::Json(obj)), Err(e) => Err(e), } } /// **Remove File/Folder** /// /// ```BASH /// curl -X POST http://localhost:8000/api/file/1/remove/ -H 'Content-Type: application/json' /// -d '{"source": ""}' -H 'Authorization: ' /// ``` #[post("/file/{id}/remove/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn remove( id: web::Path, data: web::Json, ) -> Result { match remove_file_or_folder(*id, &data.into_inner().source).await { Ok(obj) => Ok(web::Json(obj)), Err(e) => Err(e), } } /// **Upload File** /// /// ```BASH /// curl -X POST http://localhost:8000/api/file/1/upload/ -H 'Authorization: ' /// -F "file=@file.mp4" /// ``` #[put("/file/{id}/upload/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn save_file( id: web::Path, payload: Multipart, obj: web::Query, ) -> Result { upload(*id, payload, &obj.path).await }